Build an Asteroids Game With Python and Pygame

Build an Asteroids Game With Python and Pygame

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Using Pygame to Build an Asteroids Game in Python

Do you want to create your own computer games but like Python too much to abandon it for a career as a game developer? There’s a solution for that! With the Pygame module, you can use your amazing Python skills to create games, from the basic to the very complex. Below, you’ll learn how to use Pygame by making a clone of the Asteroids game!

In this tutorial, you’ll learn how to build a complete game, including:

  • Loading images and displaying them on the screen
  • Handling user input in order to control the game
  • Moving objects according to the game logic
  • Detecting collisions between objects
  • Displaying text on the screen
  • Playing sounds

Click the link below to download the code for this project and follow along as you build your game:

Let’s get started!

Demo: Asteroids Game in Python

The game you’ll be making is a clone of the classic arcade game Asteroids. In it, you control a spaceship and shoot asteroids. If your spaceship collides with an asteroid, you lose. If you shoot down all asteroids, you win!

Project Overview

Your Asteroids game in Python will feature a single spaceship. The spaceship can rotate left and right as well as accelerate forward. When it’s not accelerating, it will continue moving with the velocity it had. The spaceship can also shoot bullets.

The game will use the following key mappings:

Key Action
Right Rotate the spaceship right
Left Rotate the spaceship left
Up Accelerate the spaceship forward
Space Shoot
Esc Exit the game

There will also be six big asteroids in the game. When a bullet hits a big asteroid, it will split into two medium ones. When a bullet hits a medium asteroid, it will split into two small ones. A small asteroid won’t split but will be destroyed by a bullet.

When an asteroid collides with the spaceship, the spaceship will be destroyed, and the game will end in a defeat. When all asteroids are gone, the game will end in a victory!

The project will be broken into ten steps:

  1. Setting up Pygame for a Python project
  2. Handing input in the game
  3. Loading images and showing them on the screen
  4. Creating game objects with an image, a position, and some logic
  5. Moving the spaceship
  6. Moving the asteroids and detecting collisions with the spaceship
  7. Shooting bullets and destroying asteroids
  8. Splitting asteroids into smaller ones
  9. Playing sounds
  10. Handling the end of the game

Each step will provide links to all necessary resources.

Prerequisites

To build your Asteroids game, you’ll need some more advanced elements of Python. You should already be comfortable with the language itself as well as with concepts like classes, inheritance, and callbacks. If you need to refresh your knowledge on these topics, then check our Object-Oriented Programming (OOP) in Python 3.

The game will also use vectors to represent positions and directions, as well as some vector operations to move the elements on the screen. Pygame will take care of most of the math, and all the necessary concepts will be explained in this tutorial. However, if you want to know more, then you can check out Vector Addition.

The Pygame documentation can be useful if you want to understand some concepts in depth, but you’ll find everything you need to know in this tutorial.

Step 1: Pygame Setup

At the end of this step, you’ll have a small Python project that uses Pygame. It will display a window with a caption, filled with a blue color. This will be first step toward your Asteroids game. You won’t need any specific game development tools. Your favorite text editor and the command line will be enough.

Python Project

To organize your project, start by creating a folder for it:

$ mkdir awesome_pygame_project
$ cd awesome_pygame_project

As with any Python project, you should also create a virtual environment for your Asteroids game. You can read more about virtual environments in Python Virtual Environments: A Primer.

When you’re done, create a requirements.txt file and add a Pygame dependency. For this project, it’s recommended that you use the latest version, which will make your Asteroids game work seamlessly on Linux and macOS. Your file should look like this:

pygame==2.0.0

Next, install the dependencies:

(venv) $ python -m pip install -r requirements.txt

You can check if Pygame was installed correctly by running this command:

(venv) $ python -m pygame.examples.aliens

If everything went well, then you should see a window with the Pygame Aliens game.

Pygame Code

Now it’s time to start working on your own code! In general, the structure of a Pygame program looks like this:

 1initialize_pygame()
 2
 3while True:
 4    handle_input()
 5    process_game_logic()
 6    draw_game_elements()

Line 3 starts a loop, called the game loop. Each iteration of this loop generates a single frame of the game and usually performs the following operations:

  1. Input handling: Input like pressed buttons, mouse motion, and VR controllers position is gathered and then handled. Depending on the game, it can cause objects to change their position, create new objects, request the end of the game, and so on.

  2. Game logic: This is where most of the game mechanics are implemented. Here, the rules of physics are applied, collisions are detected and handled, artificial intelligence does its job, and so on. This part is also responsible for checking if the player has won or lost the game.

  3. Drawing: If the game hasn’t ended yet, then this is where the frame will be drawn on screen. It will include all the items that are currently in the game and are visible to the player.

The general structure of a Pygame program isn’t complicated, and you could probably get away with putting it in a basic loop. However, considering that you might expand your Asteroids game in the future, it’s a good idea to encapsulate all these operations in a Python class.

Creating a class means that you need to pick a name for your game, but “Asteroids” is already taken. How about “Space Rocks”?

Create a space_rocks directory, and inside it create a file called game.py. This is where you’ll put the main class of your Asteroids game: SpaceRocks. The file should look like this:

 1import pygame
 2
 3class SpaceRocks:
 4    def __init__(self):
 5        self._init_pygame()
 6        self.screen = pygame.display.set_mode((800, 600))
 7
 8    def main_loop(self):
 9        while True:
10            self._handle_input()
11            self._process_game_logic()
12            self._draw()
13
14    def _init_pygame(self):
15        pygame.init()
16        pygame.display.set_caption("Space Rocks")
17
18    def _handle_input(self):
19        pass
20
21    def _process_game_logic(self):
22        pass
23
24    def _draw(self):
25        self.screen.fill((0, 0, 255))
26        pygame.display.flip()

Here’s what’s happening in the code, step by step:

  • Line 1 imports the Pygame module to get access to all its amazing features.

  • Line 3 creates the SpaceRocks class.

  • Line 4 is the constructor of the SpaceRocks class, and it’s a perfect place to put any methods required to initialize Pygame. The actual Pygame initialization happens in _init_pygame(). You’ll learn more about this method in a moment.

  • Line 6 creates a display surface. Images in Pygame are represented by surfaces. Here are a few things to know about them:

    • Surfaces can be drawn on one another, allowing you to create complex scenes from simple pictures.

    • There’s one special surface in each Pygame project. That surface represents the screen and is the one that will eventually be displayed to players. All other surfaces have to be drawn on this one at some point. Otherwise, they won’t be shown.

    • To create the display surface, your program uses pygame.display.set_mode(). The only argument you pass to this method is the size of the screen, represented by a tuple of two values: width and height. In this case, Pygame will create a screen with a width of 800 pixels and a height of 600 pixels.

  • Line 8 is the game loop discussed above. It contains the same three steps for each frame:

    1. Line 10 contains input handling.

    2. Line 11 contains game logic.

    3. Line 12 contains drawing.

  • Line 14 defines a method called _init_pygame(). This is where a one-time initialization of Pygame happens. The method does two things:

    1. Line 15 calls pygame.init(). This single line of code is responsible for setting up the amazing features of Pygame. Every time you work with Pygame, you should call pygame.init() at the beginning of your program to make sure that the framework will work correctly.

    2. Line 16 sets the caption of your Pygame program using pygame.display.set_caption(). In this case, the caption will be the name of your game: Space Rocks.

  • Lines 18 and 21 define _handle_input() and _process_game_logic(). They’re empty for now, but in the following sections you’ll add some code to make your game more interesting.

  • Line 24 defines _draw(). It wouldn’t make much sense to create a template for your game without displaying anything on the screen, so this method already has some code. It’s called every frame to draw the content of the screen, and it does that in two steps:

    1. Line 25 fills the screen with a color using screen.fill(). The method takes a tuple with three values, representing three base colors: red, green, and blue. Each color value ranges between 0 and 255, representing its intensity. In this example, a tuple of (0, 0, 255) means that the color will consist only of blue, with no traces of red or green.

    2. Line 26 updates the content of the screen using pygame.display.flip(). Because your game will eventually display moving objects, you’ll call this method every frame to update the display. Because of this, you need to fill your screen with color every frame, as the method will clear the contents generated during the previous frame.

This might seem like a lot of extra steps, but now your code is nicely structured and has methods with descriptive names. Next time you need to change something related to drawing, you’ll know to use _draw(). To add input handling, you’ll modify _handle_input(), and so on.

Next, create a __main__.py file in your space_rocks folder. This file will take care of creating a new instance of your game and starting it by running main_loop(). It should look like this:

from game import SpaceRocks

if __name__ == "__main__":
    space_rocks = SpaceRocks()
    space_rocks.main_loop()

The structure of your project should now look like this:

awesome_pygame_project/
|
├── space_rocks/
|   ├── __main__.py
|   └── game.py
|
└── requirements.txt

Go ahead and run the game:

(venv) $ python space_rocks

You’ll see a window with a blue background:

Blue background

Congratulations, you just created a Pygame project! However, there’s no exit condition at this point, so you still have to use Ctrl+C in the command line to quit it. That’s why you’ll learn about input handling next.

Step 2: Input Handling

At this point, you have the main loop of the game, ready to be filled with logic. At the end of this step, you’ll also have a scaffolding in place to start plugging in user controls.

Most input processing in Pygame happens within an event loop. In each frame, your program can get a collection of events that happened since the previous frame. That includes mouse movement, keypresses, and so on. Then, one by one, these events can be handled. In Pygame, the method for obtaining that collection is pygame.event.get().

The event you need right now is pygame.QUIT. It happens when someone requests the program to end, either by clicking Close or by pressing Alt+F4 on Windows and Linux or Cmd+W on macOS. Modify space_rocks/game.py by rewriting SpaceRocks._handle_input() like so:

def _handle_input(self):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            quit()

Go ahead and test it. Run the game and click the little X in the corner or use a proper shortcut. The window will be closed, just like you would expect.

But you can go a step further. In the end, your game will be controlled with only the keyboard, not the mouse. How about closing the window by pressing a custom key?

There are other types of events in Pygame, and one of them is a keypress event. It’s represented by a pygame.KEYDOWN constant. Each such event has information about the key that was pressed stored in the event.key attribute. You can check constants for different keys in the Pygame documentation. In this example, to close the game by pressing Esc, you’ll use pygame.K_ESCAPE.

Modify the _handle_input() method again:

def _handle_input(self):
    for event in pygame.event.get():
        if event.type == pygame.QUIT or (
            event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
        ):
            quit()

Now your game also closes when you press Esc.

You’ve managed to display a window and close it properly. But the window is still filled with a single color. Next, you’ll learn how to load an image and show it on the screen.

Step 3: Images

At this point, you have a game window that you can close by pressing a key. At the end of this step, you’ll display an image in that window.

Although you could make a computer game with only colored rectangles and other simple shapes, using images will make it much more attractive. In computer game development, images are usually called sprites. Of course, games use many more types of resources, like sounds, fonts, animations, and so on. Together, these resources are called assets.

As your game grows, it’s important for it to keep a proper structure. So, start by creating a folder called assets and, inside it, another called sprites. That’s where you’ll put all the sprites used by your game.

Next, download the image of the space background and put it in the assets/sprites folder. You can download the source code by clicking the link below:

Also, because images will be loaded many times in your program, it’s a good idea to extract this functionality to a separate method in a separate file. Create a file called space_rocks/utils.py that will keep all the reusable methods. Then implement image loading:

 1from pygame.image import load
 2
 3def load_sprite(name, with_alpha=True):
 4    path = f"assets/sprites/{name}.png"
 5    loaded_sprite = load(path)
 6
 7    if with_alpha:
 8        return loaded_sprite.convert_alpha()
 9    else:
10        return loaded_sprite.convert()

Here’s what happens:

  • Line 1 imports a method called load() that will be necessary for reading images later.

  • Line 4 creates a path to an image, assuming that it’s stored in the assets/sprites directory and that it’s a PNG file. That way, you’ll only need to provide the name of the sprite later.

  • Line 5 loads the image using load(). This method returns a surface, which is an object used by Pygame to represent images. You can later draw it on the screen (or on another surface, if you want).

  • Lines 8 and 10 convert the image to a format that better fits the screen to speed up the drawing process. This is done with either convert_alpha() or convert(), depending on whether you want to use transparency.

The structure of your project now looks like this:

awesome_pygame_project/
|
├── assets/
|   |
│   └── sprites/
│       └── space.png
|
├── space_rocks/
│   ├── __main__.py
│   ├── game.py
│   └── utils.py
|
└── requirements.txt

Now that your program can load images, it’s time to change the blue background into something more interesting. Edit the space_rocks/game.py file:

import pygame

from utils import load_sprite

class SpaceRocks:
    def __init__(self):
        self._init_pygame()
        self.screen = pygame.display.set_mode((800, 600))
        self.background = load_sprite("space", False)

    def main_loop(self):
        while True:
            self._handle_input()
            self._process_game_logic()
            self._draw()

    def _init_pygame(self):
        pygame.init()
        pygame.display.set_caption("Space Rocks")

    def _handle_input(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT or (
                event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
            ):
                quit()

    def _process_game_logic(self):
        pass

    def _draw(self):
        self.screen.blit(self.background, (0, 0))
        pygame.display.flip()

To display one surface on another in Pygame, you need to call blit() on the surface you want to draw on. This method takes two arguments:

  1. The surface that you want to draw
  2. The point where you want to draw it

It’s good to keep in mind that in Pygame, the coordinate system starts in the top-left corner. The x-axis goes from left to right, and the y-axis goes from top to bottom:

Screen coordinates

As you can see, the UP vector, pointing upwards, will have a negative y-coordinate.

The coordinates passed to blit() are given as two values: X and Y. They represent the point where the top-left corner of the surface will be located after the operation:

Explanation of blitting

As you can see, the top-left corner is moved by the blit coordinates to calculate the correct position.

In your case, the new background has the same size as the screen (800 × 600 pixels), so the coordinates will be (0, 0), representing the top-left corner of the screen. That way, the background image will cover the entire screen.

Run your program now, and you’ll see a screen with a background image:

Space background

Your game now has a very nice background image, but nothing is happening there yet. Let’s change this by adding some objects.

Step 4: Controlling Game Objects

At this point, your program displays a background image of a small piece of cosmos where your Asteroids game will take place. It’s a bit empty right now, so in this section you’ll fill it up. You’ll create a class that represents other drawable game objects and use it to show a spaceship and an asteroid.

Advanced Behavior

You’ve already used surfaces, but Pygame also offers another class, Sprite, that’s intended as a base class for visible objects. It contains some useful methods, but you might also encounter a couple of limitations.

One limitation is that a game object is more than just a sprite. It contains additional data, like its direction and velocity. It also needs more advanced behaviors, like shooting bullets or playing sounds. Most of this additional information and behavior isn’t provided by the Sprite class, so you would need to add it on your own.

Another issue is that Pygame draws sprites starting from the top-left corner. In your game, it might be easier to store the central position of an object for the purpose of moving and rotating it. In that case, you’ll have to implement a way to transform that position into a top-left corner as required by Pygame.

Finally, although Pygame already has methods for detecting overlaps between images, they might not be good for detecting collisions between objects. A rotatable spaceship or an asteroid probably won’t fill the entire image, but rather the round area within it. In that case, the collision should take into account only that round area, not the entire surface of the sprite. Otherwise, you might get incorrect results:

Sprites collide, object don't

In this example, the sprites collide, but game objects don’t.

This is actually where the Sprite class could help, since you can use it with pygame.sprite.collide_circle(). This method detects collisions between two sprites using circles centered on their surfaces. However, detecting a collision of circles isn’t a very complicated process, and you can implement it on your own.

Given these issues, it quickly becomes apparent that the built-it Pygame Sprite class is meant to be augmented, not simply used on its own. In the case of your game, Pygame sprites provide few useful features. It might be a good idea to implement a custom class for game objects instead. That should give you more control and help you understand some concepts since you’ll be implementing them on your own.

GameObject Class

In this section, you’ll introduce the GameObject class. It will encapsulate some generic behavior and data for all other game objects. Classes that represent specific objects (like the spaceship) will inherit from it and extend it with their own behavior and data. If you want to refresh your knowledge of classes and inheritance, then check out Object-Oriented Programming (OOP) in Python 3.

The GameObject class will store the following data:

  • position: A point in the center of the object on the 2D screen
  • sprite: An image used to display the object
  • radius: A value representing the collision zone around the object’s position
  • velocity: A value used for movement

Here’s a graphical representation of the game object:

Game object explanation

The sprite will be a surface loaded with load_sprite() from the previous examples. The radius is an integer indicating the number of pixels from the center of the object to the edge of the collision zone. However, the position itself and the velocity will need a new type: a vector.

Vectors are similar to tuples. In a 2D world (like the one in your game), vectors are represented by two values indicating x- and y-coordinates. These coordinates can point to a position, but they can also represent motion or acceleration in a given direction. Vectors can be added, subtracted, or even multiplied to quickly update the position of a sprite. You can read more about vectors in Vectors in 2-dimensional Space.

Because of how useful vectors are in games, Pygame already has a class for them: Vector2 in the pygame.math module. It offers some additional functionality, like calculating the distance between vectors and adding or subtracting vectors. Those features will make your game logic much easier to implement.

In the space_rocks directory, create a new file called models.py. For now, it will store the GameObject class, but later you’ll add classes for asteroids, bullets, and the spaceship. The file should look like this:

 1from pygame.math import Vector2
 2
 3class GameObject:
 4    def __init__(self, position, sprite, velocity):
 5        self.position = Vector2(position)
 6        self.sprite = sprite
 7        self.radius = sprite.get_width() / 2
 8        self.velocity = Vector2(velocity)
 9
10    def draw(self, surface):
11        blit_position = self.position - Vector2(self.radius)
12        surface.blit(self.sprite, blit_position)
13
14    def move(self):
15        self.position = self.position + self.velocity
16
17    def collides_with(self, other_obj):
18        distance = self.position.distance_to(other_obj.position)
19        return distance < self.radius + other_obj.radius

Here’s a breakdown:

  • Line 1 imports the Vector2 class mentioned earlier.

  • Line 3 creates the GameObject class, which you’ll use to represent all game objects in Space Rocks.

  • Line 4 is the constructor of the GameObject class. It needs three arguments:

    1. position: The center of the object

    2. sprite: The image used to draw this object

    3. velocity: Updates the position of the object each frame

  • Lines 5 and 8 ensure that the position and the velocity will always be represented as vectors for future calculations, even if tuples are passed to the constructor. You do that by calling the Vector2() constructor. If it’s given a tuple, then it will create a new vector out of it. If it’s given a vector, then it will create a copy of that vector.

  • Line 7 calculates the radius as half the width of the sprite image. In this program, game object sprites will always be squares with transparent backgrounds. You could also use the height of the image—it would make no difference.

  • Line 10 defines draw(), which will draw the object’s sprite on the surface passed as an argument.

  • Line 11 calculates the correct position for blitting the image. The process is described in more detail below. Notice that the Vector2() constructor receives a single number instead of a tuple. In that case, it will use that number for both values. So Vector2(self.radius) is the equivalent of Vector2((self.radius, self.radius)).

  • Line 12 uses the newly calculated blit position to put your object’s sprite in a correct place on the given surface.

  • Line 14 defines move(). It will update the position of the game object.

  • Line 15 adds the velocity to the position and gets an updated position vector as a result. Pygame makes manipulating vectors straightforward, allowing you to add them like numbers.

  • Line 17 defines the collides_with() method, that will be used to detect collisions.

  • Line 18 calculates the distance between two objects by using Vector2.distance_to().

  • Line 19 checks if that distance is smaller than the sum of the objects’ radiuses. If so, the objects collide.

Please note that your game objects have a central position, but blit() requires a top-left corner. So, the blit position has to be calculated by moving the actual position of the object by a vector:

Move the blit position of GameObject

That process happens in draw().

You can test this by adding a spaceship and a single asteroid. First, copy the spaceship and asteroid images to assets/sprites. You can download the source code by clicking the link below:

The structure of your project should look like this:

awesome_pygame_project/
|
├── assets/
|   |
│   └── sprites/
│       ├── asteroid.png
│       ├── space.png
│       └── spaceship.png
|
├── space_rocks/
│   ├── __main__.py
│   ├── game.py
│   ├── models.py
│   └── utils.py
|
└── requirements.txt

Now modify the space_rocks/game.py file:

import pygame

from models import GameObject
from utils import load_sprite

class SpaceRocks:
    def __init__(self):
        self._init_pygame()
        self.screen = pygame.display.set_mode((800, 600))
        self.background = load_sprite("space", False)
        self.spaceship = GameObject(
            (400, 300), load_sprite("spaceship"), (0, 0)
        )
        self.asteroid = GameObject(
            (400, 300), load_sprite("asteroid"), (1, 0)
        )

    def main_loop(self):
        while True:
            self._handle_input()
            self._process_game_logic()
            self._draw()

    def _init_pygame(self):
        pygame.init()
        pygame.display.set_caption("Space Rocks")

    def _handle_input(self):
        for event in pygame.event.get():
            if event.type