29. Platformers

Warning

This chapter is out-dated. See the Simple Platformer Tutorial. Step 9 is a nice starting point. For more advanced usage, see Platformer with Physics.

Ever wanted to create your own platformer? It isn’t too hard! Here’s an example to get started.

29.1. Map File

29.1.1. Creating The Map

First, we need a map. This is a “map” file created with the Tiled program. The program is free. You can download it and use it to create your map file.

In this map file the numbers represent:

Number

Item

-1

Empty square

0

Crate

1

Left grass corner

2

Middle grass corner

3

Right grass corner

You can download these tiles (originally from kenney.nl) here:

../../_images/boxCrate_double1.png ../../_images/grassLeft.png ../../_images/grassMid.png ../../_images/grassRight.png

Of course, you’ll need a character to jump around the map:

../../_images/character2.png

Here is the map file:

map.csv
1-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1,2,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
2-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
30,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1,2,3,-1,-1,-1,-1,-1,-1,-1,1,2,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0
40,-1,-1,-1,1,2,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,-1,-1,-1,-1,-1,-1,0
50,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,-1,-1,-1,-1,-1,-1,-1,1,2,3,-1,-1,-1,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,0,-1,-1,-1,-1,-1,0
60,-1,-1,-1,-1,-1,-1,-1,-1,0,-1,-1,-1,-1,-1,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,-1,-1,0,-1,-1,-1,-1,-1,0,0,0,0,0,-1,-1,-1,-1,0
71,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3

The Tiled program takes some getting used to. You start off with a screen like this, and you can create a new map here:

../../_images/tiled_new_map.png

Then set up your map like this, adjusting the size of the map and the size of your images accordingly. (All your images need to be the same size. Don’t guess this number, look at the properties of the image and find how big it is.)

../../_images/tiled_new_file.png

Most of the tiles from kenney.nl are 128x128 pixels. In the image above I’ve got a 7 tile high, by 50 pixel wide side-scroll map.

After this, you have to create a new “tile set.” Find the button for that:

../../_images/tiled_new_tileset_button.png

I use these settings:

../../_images/new_tileset.png

You can add the images as tiles to your tileset. I don’t find this obvious, but you click the wrench icon, then the plus icon:

../../_images/edit_tileset.png

These “tiles” will be all the images for your map, and the numbers they associate with:

../../_images/tiled_new_tileset.png

The numbers of the tiles correspond to the order you added the tiles. I don’t think you can change the mapping after you create the tileset.

Next, you “paint” your map:

../../_images/tiled_make_map.png

When you are done, you can “Export as” a CSV file.

29.1.2. Reading The Map

Next, we want to read in this grid of numbers where each number is separated by a comma. We know how to read in a file, but how do you process a comma delimited file?

We’ve learned how to take a string and use the functions:

  • upper()

  • lower()

  • strip()

There’s another function called split(). This function will split apart a string into a list based on a delimiter. For example:

1# A comma delimited string of numbers
2my_text_string = "23,34,1,3,5,10"
3
4# Split the string into a list, based on the comma as a delimiter
5my_list = my_text_string.split(",")
6
7# Print the result
8print(my_list)

This prints:

['23', '34', '1', '3', '5', '10']

Which is close to what we want, except the list is a list of text, not numbers.

We can convert the list by:

# Convert from list of strings to list of integers
for i in range(len(my_list)):
    my_list[i] = int(my_list[i])

We haven’t covered it a lot, but you can also use enumerate to do the same thing:

# Convert from list of strings to list of integers
for index, item in enumerate(my_list):
    my_list[index] = int(item)

Or use a list comprehension:

# Convert from list of strings to list of integers
my_list = [int(item) for item in my_list]

Python does have built-in code for working with csv files. If you want, you can read about the csv library in the official documentation.

29.2. Platformer Physics Engine

In prior chapters, we’ve used the PhysicsEngineSimple to keep from running through walls. There’s another engine called PhysicsEnginePlatformer for platformers.

This engine has two important additions:

  1. Gravity

  2. can_jump method

29.2.1. Gravity

Creating the platformer physics engine requires a gravity constant. I recommend 0.5 to start with. This is your acceleration in pixels per frame.

self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
                                                     self.wall_list,
                                                     gravity_constant=GRAVITY)

29.2.2. Jumping

Also, you often need to know if there is ground beneath your character to know if she can jump. The physics engine has a method for this:

if self.physics_engine.can_jump():
    self.player_sprite.change_y = JUMP_SPEED

29.3. Python Program

In the highlighted code for the listing below, see how we’ve implemented these concepts to create a platformer

Platformer example, simple
  1"""
  2Load a map stored in csv format, as exported by the program 'Tiled.'
  3
  4Artwork from: http://kenney.nl
  5Tiled available from: http://www.mapeditor.org/
  6"""
  7import arcade
  8
  9SPRITE_SCALING = 0.5
 10
 11SCREEN_WIDTH = 800
 12SCREEN_HEIGHT = 600
 13
 14# How many pixels to keep as a minimum margin between the character
 15# and the edge of the screen.
 16VIEWPORT_MARGIN = 40
 17RIGHT_MARGIN = 150
 18
 19TILE_SIZE = 128
 20SCALED_TILE_SIZE = TILE_SIZE * SPRITE_SCALING
 21MAP_HEIGHT = 7
 22
 23# Physics
 24MOVEMENT_SPEED = 5
 25JUMP_SPEED = 14
 26GRAVITY = 0.5
 27
 28
 29def get_map(filename):
 30    """
 31    This function loads an array based on a map stored as a list of
 32    numbers separated by commas.
 33    """
 34
 35    # Open the file
 36    map_file = open(filename)
 37
 38    # Create an empty list of rows that will hold our map
 39    map_array = []
 40
 41    # Read in a line from the file
 42    for line in map_file:
 43
 44        # Strip the whitespace, and \n at the end
 45        line = line.strip()
 46
 47        # This creates a list by splitting line everywhere there is a comma.
 48        map_row = line.split(",")
 49
 50        # The list currently has all the numbers stored as text, and we want it
 51        # as a number. (e.g. We want 1 not "1"). So loop through and convert
 52        # to an integer.
 53        for index, item in enumerate(map_row):
 54            map_row[index] = int(item)
 55
 56        # Now that we've completed processing the row, add it to our map array.
 57        map_array.append(map_row)
 58
 59    # Done, return the map.
 60    return map_array
 61
 62
 63class MyWindow(arcade.Window):
 64    """ Main application class. """
 65
 66    def __init__(self):
 67        """
 68        Initializer
 69        """
 70        # Call the parent class
 71        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT)
 72
 73        # Sprite lists
 74        self.player_list = None
 75        self.wall_list = None
 76
 77        # Set up the player
 78        self.player_sprite = None
 79
 80        # Physics engine
 81        self.physics_engine = None
 82
 83        # Used for scrolling map
 84        self.view_left = 0
 85        self.view_bottom = 0
 86
 87    def setup(self):
 88        """ Set up the game and initialize the variables. """
 89
 90        # Sprite lists
 91        self.player_list = arcade.SpriteList()
 92        self.wall_list = arcade.SpriteList()
 93
 94        # Set up the player
 95        self.player_sprite = arcade.Sprite("character.png", SPRITE_SCALING)
 96
 97        # Starting position of the player
 98        self.player_sprite.center_x = 90
 99        self.player_sprite.center_y = 270
100        self.player_list.append(self.player_sprite)
101
102        # Get a 2D array made of numbers based on the map
103        map_array = get_map("map.csv")
104
105        # Now that we've got the map, loop through and create the sprites
106        for row_index in range(len(map_array)):
107            for column_index in range(len(map_array[row_index])):
108
109                item = map_array[row_index][column_index]
110
111                # For this map, the numbers represent:
112                # -1 = empty
113                # 0  = box
114                # 1  = grass left edge
115                # 2  = grass middle
116                # 3  = grass right edge
117                if item == 0:
118                    wall = arcade.Sprite("boxCrate_double.png", SPRITE_SCALING)
119                elif item == 1:
120                    wall = arcade.Sprite("grassLeft.png", SPRITE_SCALING)
121                elif item == 2:
122                    wall = arcade.Sprite("grassMid.png", SPRITE_SCALING)
123                elif item == 3:
124                    wall = arcade.Sprite("grassRight.png", SPRITE_SCALING)
125
126                if item >= 0:
127                    # Calculate where the sprite goes
128                    wall.left = column_index * SCALED_TILE_SIZE
129                    wall.top = (MAP_HEIGHT - row_index) * SCALED_TILE_SIZE
130
131                    # Add the sprite
132                    self.wall_list.append(wall)
133
134        # Create out platformer physics engine with gravity
135        self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
136                                                             self.wall_list,
137                                                             gravity_constant=GRAVITY)
138
139        # Set the background color
140        arcade.set_background_color(arcade.color.AMAZON)
141
142        # Set the view port boundaries
143        # These numbers set where we have 'scrolled' to.
144        self.view_left = 0
145        self.view_bottom = 0
146
147    def on_draw(self):
148        """
149        Render the screen.
150        """
151
152        # This command has to happen before we start drawing
153        arcade.start_render()
154
155        # Draw all the sprites.
156        self.wall_list.draw()
157        self.player_list.draw()
158
159    def on_key_press(self, key, modifiers):
160        """
161        Called whenever the mouse moves.
162        """
163        if key == arcade.key.UP:
164            # This line below is new. It checks to make sure there is a platform underneath
165            # the player. Because you can't jump if there isn't ground beneath your feet.
166            if self.physics_engine.can_jump():
167                self.player_sprite.change_y = JUMP_SPEED
168        elif key == arcade.key.LEFT:
169            self.player_sprite.change_x = -MOVEMENT_SPEED
170        elif key == arcade.key.RIGHT:
171            self.player_sprite.change_x = MOVEMENT_SPEED
172
173    def on_key_release(self, key, modifiers):
174        """
175        Called when the user presses a mouse button.
176        """
177        if key == arcade.key.LEFT or key == arcade.key.RIGHT:
178            self.player_sprite.change_x = 0
179
180    def update(self, delta_time):
181        """ Movement and game logic """
182
183        self.physics_engine.update()
184
185        # --- Manage Scrolling ---
186
187        # Track if we need to change the view port
188
189        changed = False
190
191        # Scroll left
192        left_bndry = self.view_left + VIEWPORT_MARGIN
193        if self.player_sprite.left < left_bndry:
194            self.view_left -= left_bndry - self.player_sprite.left
195            changed = True
196
197        # Scroll right
198        right_bndry = self.view_left + SCREEN_WIDTH - RIGHT_MARGIN
199        if self.player_sprite.right > right_bndry:
200            self.view_left += self.player_sprite.right - right_bndry
201            changed = True
202
203        # Scroll up
204        top_bndry = self.view_bottom + SCREEN_HEIGHT - VIEWPORT_MARGIN
205        if self.player_sprite.top > top_bndry:
206            self.view_bottom += self.player_sprite.top - top_bndry
207            changed = True
208
209        # Scroll down
210        bottom_bndry = self.view_bottom + VIEWPORT_MARGIN
211        if self.player_sprite.bottom < bottom_bndry:
212            self.view_bottom -= bottom_bndry - self.player_sprite.bottom
213            changed = True
214
215        # If we need to scroll, go ahead and do it.
216        if changed:
217            arcade.set_viewport(self.view_left,
218                                SCREEN_WIDTH + self.view_left,
219                                self.view_bottom,
220                                SCREEN_HEIGHT + self.view_bottom)
221
222
223def main():
224    window = MyWindow()
225    window.setup()
226
227    arcade.run()
228
229
230main()

29.4. Other Examples