Build a Platform Game in Python With Arcade

Build a Platform Game in Python With Arcade

by Jon Fincher Updated Reading time estimate 1h 20m intermediate gamedev

For many video game players, the lure of writing games is a prime reason to learn computer programming. However, building a 2D platform game such as Lode Runner, Pitfall!, or Super Mario Bros. without proper tools or guidance can leave you frustrated. Fortunately, the Python arcade library makes creating a 2D game in Python accessible for many programmers!

If you haven’t already heard about it, the arcade library is a modern Python framework for crafting games with compelling graphics and sound. Object oriented and built for Python 3.6 and above, arcade provides you with a modern set of tools for crafting great game experiences, including platform games.

By the end of this tutorial, you’ll be able to:

  • Install the Python arcade library
  • Create a basic 2D game structure
  • Find usable game artwork and other assets
  • Build platform maps using the Tiled map editor
  • Define player actions, game rewards, and obstacles
  • Control your player with keyboard and joystick input
  • Play sound effects for game actions
  • Scroll the game screen with viewports to keep your player in view
  • Add title, instruction, and pause screens
  • Move nonplayer game elements on the screen

This tutorial assumes you have a basic understanding of writing Python programs. You should also be comfortable using the arcade library and familiar with object-oriented Python, which is used extensively in arcade.

You can download all the code, images, and sounds for this tutorial by clicking the link below:

Installing Python arcade

You can install arcade and its dependencies using pip:

Language: Shell
$ python -m pip install arcade

Complete installation instructions are available for Windows, Mac, and Linux. You can even install arcade directly from source if you’d prefer.

This tutorial uses Python 3.9 and arcade 2.5.5 throughout.

Designing the Game

Before you begin writing any code, it’s beneficial to have a plan in place. Since your goal is to write a 2D platform game, it would be a good idea to define exactly what makes a game a platformer.

What Is a Platform Game?

There are a few characteristics that separate platform games from other types of games:

  • The player jumps and climbs between various platforms on the game field.
  • The platforms often feature uneven terrain and uneven height placements.
  • Obstacles are placed in the player’s path and must be overcome to reach a goal.

These are just the minimum requirements for a platform game, and you’re free to add other features as you see fit, including:

  • Multiple levels of increasing difficulty
  • Rewards available throughout the game
  • Multiple player lives
  • Ability to destroy game obstacles

The game plan developed in this tutorial includes increasing difficulty and rewards.

Game Story

All good games have some backstory to them, even if it’s a simple one:

Your game benefits from a story that connects the actions taken by the player to some overarching goal.

For this tutorial, the game story concerns a space traveler named Roz, who has crash-landed on an alien world. Before their craft crashed, Roz was thrown clear and now needs to find their space ship, fix it, and return home.

To do this, Roz must travel from their current location to the exit of each level, which brings them closer to the ship. Along the way, Roz can collect coins, which are used to fix the damaged craft. Since Roz was ejected from the ship, they don’t have any weapons and so must avoid any dangerous obstacles in the way.

While this story may seem silly, it serves the important purpose of informing the design of your levels and characters. This helps you make decisions as you implement features:

  • Since Roz has no weapons, there is no way to shoot enemies that may appear.
  • Roz crashed on an alien world, so enemies can be anywhere and anything.
  • Because the planet is alien, gravity can be different, which may affect Roz’s jump and movement abilities.
  • Roz needs to fix their damaged spaceship, which requires collecting items to do so. Right now, coins are available, but other items may be available later.

When designing a game, you can make the story as simple or involved as you like.

Game Mechanics

With a rough design in mind, you can also begin planning how you will control gameplay. Moving Roz around the game field requires a way to control several different movements:

  • Left and Right to move on a platform
  • Up and Down to climb ladders between platforms
  • Jump to collect coins, avoid enemies, or move between platforms

Traditionally, players are controlled using the four arrow keys for directional movement, along with Space for jumping. You can also use keys such as IJKL, IJKM, or WASD if you’d like.

You aren’t limited to just keyboard input, either. The arcade library includes support for joysticks and game controllers, which you’ll explore later. Once a joystick is connected to your computer, you can move Roz by checking the position of the X- and Y-axis of the stick and jump by checking for specific button presses.

Game Assets

Now that you have an idea of how the game should work, you need to make some decisions about how the game will look and sound. The images, sprites, sounds, and even text used to display the score are collectively known as assets. They define your game in the eyes of your players. Creating them can be a challenge, taking as much time, if not more, than writing the actual game code.

Rather than create your own assets, you can download free or low-cost assets to use in your game. Many artists and designers provide sprites, backgrounds, fonts, sounds, and other content for game makers to use. Here are some sources for music, sound, and art that you can search for useful content:

Source Sprites Artwork Music Sound Effects
OpenGameArt.org X X X X
Kenney.nl X X X X
Game Art 2D X X
ccMixter X X
Freesound X X

For the game outlined in this tutorial, you’ll use freely available map tile images and sprites created by Kenney.nl. Sound effects provided in the downloadable source code were created by the author using MuseScore and Audacity.

The final step before you can begin writing code is deciding how you will structure and store everything.

Defining the Program Structure

Because video games consist of graphic and sound assets as well as code, it’s important to organize your project. Keeping game assets and code properly organized will allow you to make targeted changes to the design or behavior of your game while minimizing the impact to other game aspects.

The project uses the following structure:

arcade_platformer/
|
├── arcade_platformer/
|
├── assets/
|   |
│   ├── images/
|   |   |
│   │   ├── enemies/
|   |   |
│   │   ├── ground/
|   |   |
│   │   ├── HUD/
|   |   |
│   │   ├── items/
|   |   |
│   │   ├── player/
|   |   |
│   │   └── tiles/
|   |
│   └── sounds/
|
└── tests/

Under the root folder of the project are the following subfolders:

  • arcade_platformer holds all the Python code for the game.
  • assets consists of all your game images, fonts, sounds, and tile maps.
  • tests contains any tests you may choose to write.

While there are some other gameplay decisions to be made, this is enough to begin writing code. You’ll get started by defining the basic arcade code structure in which you can build your platform game!

Defining the Game Structure in Python arcade

Your game uses the full object-oriented capabilities of arcade. To do so, you define a new class based on arcade.Window, then override methods in that class to update and render your game graphics.

Here’s a basic skeleton of what a finished game might look like. You will build on this skeleton as the game progresses:

Language: Python
 1"""
 2Arcade Platformer
 3
 4Demonstrating the capabilities of arcade in a platformer game
 5Supporting the Arcade Platformer article
 6at https://realpython.com/platformer-python-arcade/
 7
 8All game artwork from www.kenney.nl
 9Game sounds and tile maps by author
10"""
11
12import arcade
13
14class Platformer(arcade.Window):
15    def __init__(self):
16        pass
17
18    def setup(self):
19        """Sets up the game for the current level"""
20        pass
21
22    def on_key_press(self, key: int, modifiers: int):
23        """Processes key presses
24
25        Arguments:
26            key {int} -- Which key was pressed
27            modifiers {int} -- Which modifiers were down at the time
28        """
29
30    def on_key_release(self, key: int, modifiers: int):
31        """Processes key releases
32
33        Arguments:
34            key {int} -- Which key was released
35            modifiers {int} -- Which modifiers were down at the time
36        """
37
38    def on_update(self, delta_time: float):
39        """Updates the position of all game objects
40
41        Arguments:
42            delta_time {float} -- How much time since the last call
43        """
44        pass
45
46    def on_draw(self):
47        pass
48
49if __name__ == "__main__":
50    window = Platformer()
51    window.setup()
52    arcade.run()

This basic structure provides almost everything you need to construct a 2D platformer game:

  • Line 12 imports the arcade library.

  • Line 14 defines the class used to run the entire game. Methods of this class are called to update game state, process user input, and draw items on the screen.

  • Line 15 defines .__init__(), which initializes the game object. You add code here to handle actions that should only be taken when the game first starts.

  • Line 18 defines .setup(), which sets up the game to begin playing. You add code to this method that may need to be repeated throughout the game. For example, this a great place to initialize new levels on success or reset the current level on failure.

  • Lines 22 and 30 define .on_key_press() and .on_key_release(), which allow you to process keyboard input independently. arcade treats key presses and key releases separately, which helps avoid problems with keyboard auto-repeat.

  • Line 38 defines .on_update(), where you update the state of your game and all the objects in it. This is where collisions between objects are handled, most sound effects are played, scores are updated, and sprites are animated. This method is where everything in your game actually happens, so there is usually a lot of code here.

  • Line 46 defines .on_draw(), where everything displayed in your game is drawn. In contrast to .on_update(), this method usually contains only a few lines of code.

  • Lines 49 to 52 define the main entry point for your game. This is where you:

    • Create the game object window based on your class defined on line 13
    • Set up the game by calling window.setup()
    • Kick off the game loop by calling arcade.run()

This basic structure works well for most Python arcade games.

As you progress through this tutorial, you’ll flesh out each of these methods and add new ones to implement your game’s functionality.

Adding Initial Game Functionality

The first thing to do when starting the game is to open the game window. By the end of this section, your game will look something like this:

Running the game for the first time.

You can see the changes to your game skeleton in arcade_platformer/02_open_game_window.py:

Language: Python
11import arcade
12import pathlib
13
14# Game constants
15# Window dimensions
16SCREEN_WIDTH = 1000
17SCREEN_HEIGHT = 650
18SCREEN_TITLE = "Arcade Platformer"
19
20# Assets path
21ASSETS_PATH = pathlib.Path(__file__).resolve().parent.parent / "assets"
22
23class Platformer(arcade.Window):
24    def __init__(self) -> None:
25        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
26
27        # These lists will hold different sets of sprites
28        self.coins = None
29        self.background = None
30        self.walls = None
31        self.ladders = None
32        self.goals = None
33        self.enemies = None
34
35        # One sprite for the player, no more is needed
36        self.player = None
37
38        # We need a physics engine as well
39        self.physics_engine = None
40
41        # Someplace to keep score
42        self.score = 0
43
44        # Which level are we on?
45        self.level = 1
46
47        # Load up our sounds here
48        self.coin_sound = arcade.load_sound(
49            str(ASSETS_PATH / "sounds" / "coin.wav")
50        )
51        self.jump_sound = arcade.load_sound(
52            str(ASSETS_PATH / "sounds" / "jump.wav")
53        )
54        self.victory_sound = arcade.load_sound(
55            str(ASSETS_PATH / "sounds" / "victory.wav")
56        )

Here’s a breakdown:

  • Lines 11 and 12 import the arcade and pathlib libraries you need.

  • Lines 16 to 18 define several game window constants that are used to open the game window later.

  • Line 21 saves the path to your assets folder, using the path of the current file as a base. Since you will be using these assets throughout the game, knowing where they are is vital. Using pathlib ensures your paths will work correctly on Windows, Mac, or Linux.

  • Line 25 sets up your game window by calling the parent class’ .__init__() method using super() and the constants defined above on lines 16 to 18.

  • Lines 28 to 33 define six different sprite lists to hold the various sprites used in the game. It’s not strictly necessary to declare and define these here, as they will be properly and fully defined later in .setup(). Declaring object properties is a holdover from languages like C++ or Java. Each level will have a different set of objects, which are populated in .setup():

    • coins are collectible objects Roz can find throughout the game.

    • background objects are presented for visual interest only and don’t interact with anything.

    • walls are objects that Roz can’t move through. These include actual walls and the platforms on which Roz walks and jumps.

    • ladders are objects that allow Roz to climb up or down.

    • goals are objects Roz must find to move to the next level.

    • enemies are objects Roz must avoid throughout the game. Contact with an enemy will end the game.

  • Line 36 declares the player object, which will be properly defined in .setup().

  • Line 39 declares a physics engine that is used to manage movement and collisions.

  • Line 42 defines a variable to track the current score.

  • Line 45 defines a variable to track the current game level.

  • Lines 48 to 56 use the ASSETS_PATH constant defined earlier to locate and load the sound files used for collecting coins, jumping, and finishing each level.

You can add more here if you wish, but remember that .__init__() is only run when the game first starts.

Roz needs to be able to walk, jump, and climb around the game world. Managing when and how that happens is the job of the physics engine.

What Is a Physics Engine?

In most platformers, the user moves the player using a joystick or the keyboard. They might make the player jump or walk the player off a platform. Once the player is in midair, the user doesn’t need to do anything else to make them fall to a lower platform. Controlling where a player can walk and how they fall after they jump or walk off a platform is handled by the physics engine.

In a game, the physics engine provides an approximation of the physical forces that act on players and other game objects. These forces may impart or impact the movement of game objects, including jumping, climbing, falling, and blocking movement.

There are three physics engines included in Python arcade:

  1. arcade.PhysicsEngineSimple is a very basic engine that handles the movement and interactions of a single player sprite and a sprite list of walls. This is useful for top-down games, where gravity is not a factor.

  2. arcade.PhysicsEnginePlatformer is a more complex engine tailored for use in platform games. In addition to basic movement, it provides a gravity force that pulls objects to the bottom of the screen. It also provides the player a way to jump and climb ladders.

  3. arcade.PymunkPhysicsEngine is built on top of Pymunk, a 2D physics library that uses the Chipmunk library. Pymunk makes extremely realistic physics calculations available to arcade applications.

For this tutorial you will use the arcade.PhysicsEnginePlatformer.

In order to properly set up the arcade.PhysicsEnginePlatformer, you must provide the player sprite as well as two sprite lists containing the walls and ladders with which the player interacts. Since the walls and ladders vary based on the level, you can’t define the physics engine formally until the level is set up, which happens in .setup().

Speaking of levels, how do you define those anyway? As with most things, there’s more than one way to get the job done.

Building Game Levels

Back when video games were still distributed on floppy disks, it was difficult to store all the game level data needed for a game. Many game makers resorted to writing code to create levels. While this method saves disk space, using imperative code to generate game levels limits your ability to modify or augment them later.

As storage space became less expensive, games took advantage by storing more of their assets in data files, which were read and processed by the code. Game levels could now be created and modified without changing the game code, which allowed artists and game designers to contribute without needing to understand the underlying code. This declarative method of level design allows for more flexibility when designing and developing games.

The disadvantage to declarative game level design is the need to not only define the data but store it as well. Fortunately, there’s a tool available that can do both, and it works extremely well with arcade.

Tiled is an open source 2D game level editor that produces files that can be read and used by Python arcade. Tiled allows you to create a collection of images called a tileset, which is used to create a tile map defining each level of your game. You can use Tiled to create tile maps for top-down, isometric, and side-scrolling games, including the levels for your game:

Basic design for level one of the arcade platformer

Tiled comes with a great set of docs and a great intro tutorial as well. To get you started and hopefully whet your appetite for more, next you’ll walk through the steps to create your first map level.

Downloading and Starting Tiled

Before you run Tiled, you need to download it. The current version at the time of writing was Tiled version 1.4.3, which was available for Windows, Mac, and Linux in a variety of formats. When downloading, consider supporting its continued maintenance by making a donation as well.

Once you’ve downloaded Tiled, you can start it for the first time. You’ll see the following window:

Tiled, the platformer editor, on first start

Click New Map to create the tile map for your first level. The following dialog will appear:

Creating a new tile map in Tiled

These default tile map properties are great for platform games and represent the best options for an arcade game. Here’s a quick breakdown of other options you can select:

  • Orientation specifies how the map is displayed and edited.
    • Orthogonal maps are square and are used for top-down and platform games. arcade works best with orthogonal maps.
    • Isometric maps shift the viewpoint to be a nonsquare angle to the game field, providing a pseudo-3D view of the 2D world. Staggered isometric maps specify that the top edge of the map is the top edge of the view.
    • Hexagonal maps use hexagons rather than squares for each map tile (although Tiled displays squares in the editor).
  • Tile layer format specifies how the map is stored on disk. Compression using zlib helps conserve disk space.
  • Tile render order specifies how tiles are stored in the file and ultimately how they’re rendered by the game engine.
  • Map size sets the size of the map to be stored, in tile units. Specifying the map as Infinite tells Tiled to determine the final size based on the edits made.
  • Tile size specifies the size of each tile in pixels. If you’re using artwork from an external source, set this to the size of the tiles in that set. The artwork provided for this tutorial uses square sprites that measure 128 × 128 pixels. This means that every tile consists of around 16,000 pixels and that they can be stored on disk and in memory in a way that can increase game performance if necessary.

Click Save As to save the level. Since this is a game asset, save it as arcade_platformer/assets/platform_level_01.tmx.

Tile maps consist of a set of tiles that are placed on specific map layers. To begin defining a tile map for a level, you must first define the tileset to use and the layers on which they appear.

Creating a Tileset

The tiles used to create your level are contained in a tileset. The tileset is associated with the tile map and provides all the sprite images required to define the level.

You define and interact with a tileset using the Tilesets view, located in the lower-right corner of the Tiled window:

Location of the tileset in Tiled

Click the New Tileset button to define the tileset for this level. Tiled presents a dialog asking for some information about the new tileset to create:

Creating a new tile set in Tiled

You have the following options for your new tileset:

  • Name is the name of your tileset. Call this one arcade_platformer.
  • Type specifies how the tileset will be defined:
    • Collection of Images indicates that each tile is contained in a single, separate image on disk. You should select this option, as arcade works best with individual tile images.
    • Based on Tileset Image indicates that all the tiles are combined into one single large image that Tiled needs to process to locate each individual image. Only select this option if the assets you are using require it.
  • Embed in Map tells Tiled to store the tileset in the tile map. Keep this unchecked, as you will save and use the tileset as a separate resource in multiple tile maps.

Click Save As and save it as assets/arcade_platformer.tsx. To reuse this tileset on future tile maps, select Map → Add External Tileset to include it.

Defining the Tileset

Your new tileset is initially empty, so you need to populate it with tiles. You do this by locating your tile images and adding them to the set. Each image should be the same dimensions as the Tile size you defined when you created the tile map.

This example assumes you have downloaded the game assets for this tutorial. You can do so by clicking the link below:

Alternatively, you can download the Platformer Pack Redux (360 Assets) and move the contents of the PNG folder to your arcade-platformer/assets/images folder. Recall that your tile map is located under arcade-platformer/assets, as this will be important later.

On the toolbar, click the blue plus sign (+) or select Tileset → Add Tiles to begin the process. You will be presented with the following dialog:

Adding tiles to a tile set in Tiled

From here, navigate to the folders listed below to add the specified resources to your tileset:

Folder File
arcade-platformer/assets/images/ground/Grass All Files
arcade-platformer/assets/images/HUD hudHeart_empty.png
hudHeart_full.png
hudHeart_half.png
hudX.png
arcade-platformer/assets/images/items coinBronze.png
coinGold.png
coinSilver.png
flagGreen_down.png
flagGreen1.png
flagGreen2.png
arcade-platformer/assets/images/tiles doorOpen_mid.png
doorOpen_top.png
grass.png
ladderMid.png
ladderTop.png
signExit.png
signLeft.png
signRight.png
torch1.png
torch2.png
water.png
waterTop_high.png
waterTop_low.png

When you’re done adding files, your tileset should look like this:

The populated tile set in Tiled

If you don’t see all your tiles, click the Dynamically Wrap Tiles button on the toolbar to show them all.

Save your new tileset using Ctrl+S or File → Save from the menu and return to your tile map. You’ll see the new tileset in the lower right of the Tiled interface, ready for use in defining your tile map!

Defining Map Layers

Every item in a level serves a specific purpose:

  • Ground and walls define where and how your player can move.
  • Coins and other collectible items score points and unlock achievements.
  • Ladders allow the player to climb to new platforms but don’t otherwise block movement.
  • Background items provide visual interest and may provide information.
  • Enemies provide obstacles for the player to avoid.
  • Goals provide a reason to move around the level.

Each of these different item types requires different handling in arcade. Therefore, it makes sense to keep them separate when defining them in Tiled. Tiled allows you to do just that by using map layers. By placing different item types on different map layers and processing each layer separately, you can track and handle each type of sprite differently.

To define a layer, first open the Layers view in the upper-right corner of the Tiled screen:

The Layers view in Tiled

The default layer is already set and selected. Rename this layer as ground by clicking the layer, then changing the Name in the Properties view on the left. Alternatively, you can double-click the name to edit it directly in the Layers panel:

Changing a layer name in Tiled

This layer will contain your ground tiles, including walls through which the player can’t walk.

Creating new layers requires you to define not only the layer name but also the layer type. Tiled provides four types of layers:

  1. Tile layers allow you to place tiles from your tileset onto the map. Placement is restricted to grid locations, and tiles must be placed as defined.
  2. Object layers allow you to place objects such as collectibles or triggers on the map. Objects may be tiles from the tile map or freely drawn shapes, and they may be visible or not. Each object can be freely positioned, scaled, and rotated.
  3. Image layers allow you to place images onto the map for use as background or foreground imagery.
  4. Group layers allow you to gather layers into groups for easier map management.

For this tutorial, you’ll use an object layer to place coins on the map and tile layers for everything else.

To create the new tile layers, click New Layer in the Layers view, then select Tile Layer:

Creating a new map layer in Tiled

Create three new tile layers named ladders, background, and goal.

Next, create a new object layer called coins to hold your collectibles:

Creating a new object map layer in Tiled

You can arrange the layers in any order you like using the arrow buttons at the bottom of the layer view. Now you can start laying out your level!

Designing a Level

In the book Classic Game Design, author and game developer Franz Lanzinger defines eight rules for classic game design. Here are the first three rules:

  1. Keep it simple.
  2. Start gameplay immediately.
  3. Ramp difficulty from easy to hard.

Similarly, veteran game developer Steve Goodwin talks about balancing games in his book Polished Game Development. He stresses that good game balance starts with level 1, which “should be the first one developed and the last one finished.”

With these ideas in mind, here are some guidelines for designing your platformer levels:

  1. The first level of the game should introduce the user to basic game features and controls.
  2. Make the initial obstacles easy to overcome.
  3. Make the first collectibles impossible to miss and later ones more difficult to obtain.
  4. Don’t introduce obstacles that require finesse to overcome until the user has learned to navigate the world.
  5. Don’t introduce enemies until the user has learned to overcome obstacles.

Below is a closer look at a first level designed with these guidelines in mind. In the downloadable materials, this complete level design is found under assets/platform_level_01.tmx:

Basic design for level one of the arcade platformer

The player starts on the left and proceeds to the right, indicated by the arrow pointing to the right. As the player moves right, they find a bronze coin, which will increase their score. A second bronze coin is found later hanging higher in the air, which demonstrates to the player that coins may be anywhere. Then the player finds a gold coin, which has a different point value.

The player then climbs a ramp, which demonstrates that there is more of the world above them. At the top of the hill is the final gold coin, which they have to jump to get. On the other side of the hill is the exit, which is also marked.

This simple level helps show the user how to move and jump. It shows that there are collectible items in the world worth points. It also shows items that are informative or decorative and with which the player does not interact, such as the arrow sign, exit sign, and grass tufts. Finally, it shows them what the goal looks like.

With the hard work of designing your first level complete, you can now build it in Tiled.

Building a Level

Before you can place coins and the goal, you need to know how to get there. So the first thing to define is where the ground is located. With your tile map selected in Tiled, select the ground layer to build.

From your tileset, select the grassCenter tile. Then, click in any grid on the bottom row of your tile map to set that tile in place:

Setting the first ground tile in Tiled

With the first tileset, you can drag across the bottom row to set everything to grassCenter. Then, select the grassMid tile to draw the grassy top of the level across the second row:

Placing grass tiles in Tiled

Continue building the level using the grass tiles to build a two-tile-high hill starting about halfway through the world. Leave a space of four tiles at the right edge to provide room for the player to walk down the hill and for the exit sign and exit portal.

Next, switch to the goal layer and place the exit portal tiles one tile in from the far-right edge:

Placing the goal in Tiled

With the basic platform and goal in place, you can place some background items. Switch to the background layer, place an arrow on the left side to direct the player where to go and an Exit sign