Step 10 - Multiple Levels and Other Layers#

Now that we’ve seen the basics of loading a Tiled map, we’ll give another example with some more features. In this example we’ll add the following things:

  • New layers including foreground, background, and “Don’t Touch”

    • The background layer will appear behind the player

    • The foreground layer will appear in front of the player

    • The Don’t Touch layer will cause the player to be reset to the start

  • The player resets to the start if they fall off the map

  • If the player gets to the right side of the map, the program attempts to load the next map

    • This is achieved by naming the maps with incrementing numbers, something like “map_01.json”, “map_02.json”, etc. Then having a level attribute to track which number we’re on and increasing it and re-running the setup function.

To start things off, let’s add a few constants at the top of our game. The first one we need to define is the size of a sprite in pixels. Along with that we need to know the grid size in pixels. These are used to calculate the end of the level.

Multiple Levels - Constants#
TILE_SCALING = 0.5
COIN_SCALING = 0.5

Next we need to define a starting position for the player, and then since we’re starting to have a larger number of layers in our game, it will be best to store their names in variables in case we need to change them later.

Multiple Levels - Constants#
PLAYER_JUMP_SPEED = 20

# Player starting position
PLAYER_START_X = 64
PLAYER_START_Y = 225

# Layer Names from our TileMap
LAYER_NAME_PLATFORMS = "Platforms"
LAYER_NAME_COINS = "Coins"
LAYER_NAME_FOREGROUND = "Foreground"

Then in the __init__ function we’ll add two new values. One to know where the right edge of the map is, and one to keep track of what level we’re on, and add a new game over sound.

Multiple Levels - Init Function#
        self.reset_score = True

        # Where is the right edge of the map?
        self.end_of_map = 0

        # Level
        self.level = 1

        # Load sounds
        self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")

Also in our __init__ function we’ll need a variable to tell us if we need to reset the score. This will be the case if the player fails the level. However, now that the player can pass a level, we need to keep the score when calling our setup function for the new level. Otherwise it will reset the score back to 0

Multiple Levels - Init Function#
        self.score = 0

        # Do we need to reset the score?

Then in our setup function we’ll change up our map name variable to use that new level attribute, and add some extra layer specific options for the new layers we’ve added to our map.

Multiple Levels - Setup Function#
        self.camera = arcade.SimpleCamera(viewport=viewport)
        self.gui_camera = arcade.SimpleCamera(viewport=viewport)

        # Map name
        map_name = f":resources:tiled_maps/map2_level_{self.level}.json"

        # Layer Specific Options for the Tilemap
        layer_options = {
            LAYER_NAME_PLATFORMS: {
                "use_spatial_hash": True,
            },
            LAYER_NAME_COINS: {
                "use_spatial_hash": True,
            },
            LAYER_NAME_DONT_TOUCH: {

Now in order to make our player appear behind the “Foreground” layer, we need to add a line in our setup function before we create the player Sprite. This will basically be telling our Scene where in the render order we want to place the player. Previously we haven’t defined this, and so it’s always just been added to the end of the render order.

Multiple Levels - Setup Function#
            self.score = 0
        self.reset_score = True

        # Add Player Spritelist before "Foreground" layer. This will make the foreground
        # be drawn after the player, making it appear to be in front of the Player.
        # Setting before using scene.add_sprite allows us to define where the SpriteList
        # will be in the draw order. If we just use add_sprite, it will be appended to the
        # end of the order.
        self.scene.add_sprite_list_after("Player", LAYER_NAME_FOREGROUND)

        # Set up the player, specifically placing it at these coordinates.
        image_source = ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
        self.player_sprite = arcade.Sprite(image_source, CHARACTER_SCALING)

Next in our setup function we need to check to see if we need to reset the score or keep it.

Multiple Levels - Setup Function#
        # Load in TileMap
        self.tile_map = arcade.load_tilemap(map_name, TILE_SCALING, layer_options)

        # Initiate New Scene with our TileMap, this will automatically add all layers
        # from the map as SpriteLists in the scene in the proper order.
        self.scene = arcade.Scene.from_tilemap(self.tile_map)

        # Keep track of the score, make sure we keep the score if the player finishes a level

Lastly in our setup function we need to calculate the end_of_map value we added earlier in init.

Multiple Levels - Setup Function#

        # --- Load in a map from the tiled editor ---

The on_draw, on_key_press, and on_key_release functions will be unchanged for this section, so the last thing to do is add a few things to the on_update function. First we check if the player has fallen off of the map, and if so, we move them back to the starting position. Then we check if they collided with something from the “Don’t Touch” layer, and if so reset them to the start. Lastly we check if they’ve reached the end of the map, and if they have we increment the level value, tell our setup function not to reset the score, and then re-run the setup function.

Multiple Levels - Update Function#
            # Add one to the score
            self.score += 1

        # Did the player fall off the map?
        if self.player_sprite.center_y < -100:
            self.player_sprite.center_x = PLAYER_START_X
            self.player_sprite.center_y = PLAYER_START_Y

            arcade.play_sound(self.game_over)

        # Did the player touch something they should not?
        if arcade.check_for_collision_with_list(
            self.player_sprite, self.scene[LAYER_NAME_DONT_TOUCH]
        ):
            self.player_sprite.change_x = 0
            self.player_sprite.change_y = 0
            self.player_sprite.center_x = PLAYER_START_X
            self.player_sprite.center_y = PLAYER_START_Y

            arcade.play_sound(self.game_over)

        # See if the user got to the end of the level
        if self.player_sprite.center_x >= self.end_of_map:
            # Advance to the next level
            self.level += 1

            # Make sure to keep the score from this level when setting up the next level
            self.reset_score = False

Source Code#

Multiple Levels#
  1"""
  2Platformer Game
  3
  4python -m arcade.examples.platform_tutorial.10_multiple_levels
  5"""
  6import arcade
  7
  8# Constants
  9SCREEN_WIDTH = 1000
 10SCREEN_HEIGHT = 650
 11SCREEN_TITLE = "Platformer"
 12
 13# Constants used to scale our sprites from their original size
 14CHARACTER_SCALING = 1
 15TILE_SCALING = 0.5
 16COIN_SCALING = 0.5
 17SPRITE_PIXEL_SIZE = 128
 18GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING
 19
 20# Movement speed of player, in pixels per frame
 21PLAYER_MOVEMENT_SPEED = 10
 22GRAVITY = 1
 23PLAYER_JUMP_SPEED = 20
 24
 25# Player starting position
 26PLAYER_START_X = 64
 27PLAYER_START_Y = 225
 28
 29# Layer Names from our TileMap
 30LAYER_NAME_PLATFORMS = "Platforms"
 31LAYER_NAME_COINS = "Coins"
 32LAYER_NAME_FOREGROUND = "Foreground"
 33LAYER_NAME_BACKGROUND = "Background"
 34LAYER_NAME_DONT_TOUCH = "Don't Touch"
 35
 36
 37class MyGame(arcade.Window):
 38    """
 39    Main application class.
 40    """
 41
 42    def __init__(self):
 43
 44        # Call the parent class and set up the window
 45        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 46
 47        # Our TileMap Object
 48        self.tile_map = None
 49
 50        # Our Scene Object
 51        self.scene = None
 52
 53        # Separate variable that holds the player sprite
 54        self.player_sprite = None
 55
 56        # Our physics engine
 57        self.physics_engine = None
 58
 59        # A Camera that can be used for scrolling the screen
 60        self.camera = None
 61
 62        # A Camera that can be used to draw GUI elements
 63        self.gui_camera = None
 64
 65        # Keep track of the score
 66        self.score = 0
 67
 68        # Do we need to reset the score?
 69        self.reset_score = True
 70
 71        # Where is the right edge of the map?
 72        self.end_of_map = 0
 73
 74        # Level
 75        self.level = 1
 76
 77        # Load sounds
 78        self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
 79        self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
 80        self.game_over = arcade.load_sound(":resources:sounds/gameover1.wav")
 81
 82    def setup(self):
 83        """Set up the game here. Call this function to restart the game."""
 84
 85        # Set up the Cameras
 86        viewport = (0, 0, self.width, self.height)
 87        self.camera = arcade.SimpleCamera(viewport=viewport)
 88        self.gui_camera = arcade.SimpleCamera(viewport=viewport)
 89
 90        # Map name
 91        map_name = f":resources:tiled_maps/map2_level_{self.level}.json"
 92
 93        # Layer Specific Options for the Tilemap
 94        layer_options = {
 95            LAYER_NAME_PLATFORMS: {
 96                "use_spatial_hash": True,
 97            },
 98            LAYER_NAME_COINS: {
 99                "use_spatial_hash": True,
100            },
101            LAYER_NAME_DONT_TOUCH: {
102                "use_spatial_hash": True,
103            },
104        }
105
106        # Load in TileMap
107        self.tile_map = arcade.load_tilemap(map_name, TILE_SCALING, layer_options)
108
109        # Initiate New Scene with our TileMap, this will automatically add all layers
110        # from the map as SpriteLists in the scene in the proper order.
111        self.scene = arcade.Scene.from_tilemap(self.tile_map)
112
113        # Keep track of the score, make sure we keep the score if the player finishes a level
114        if self.reset_score:
115            self.score = 0
116        self.reset_score = True
117
118        # Add Player Spritelist before "Foreground" layer. This will make the foreground
119        # be drawn after the player, making it appear to be in front of the Player.
120        # Setting before using scene.add_sprite allows us to define where the SpriteList
121        # will be in the draw order. If we just use add_sprite, it will be appended to the
122        # end of the order.
123        self.scene.add_sprite_list_after("Player", LAYER_NAME_FOREGROUND)
124
125        # Set up the player, specifically placing it at these coordinates.
126        image_source = ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
127        self.player_sprite = arcade.Sprite(image_source, CHARACTER_SCALING)
128        self.player_sprite.center_x = PLAYER_START_X
129        self.player_sprite.center_y = PLAYER_START_Y
130        self.scene.add_sprite("Player", self.player_sprite)
131
132        # --- Load in a map from the tiled editor ---
133
134        # Calculate the right edge of the my_map in pixels
135        self.end_of_map = self.tile_map.width * GRID_PIXEL_SIZE
136
137        # --- Other stuff
138        # Set the background color
139        if self.tile_map.background_color:
140            arcade.set_background_color(self.tile_map.background_color)
141
142        # Create the 'physics engine'
143        self.physics_engine = arcade.PhysicsEnginePlatformer(
144            self.player_sprite,
145            gravity_constant=GRAVITY,
146            walls=self.scene[LAYER_NAME_PLATFORMS],
147        )
148
149    def on_draw(self):
150        """Render the screen."""
151
152        # Clear the screen to the background color
153        self.clear()
154
155        # Activate the game camera
156        self.camera.use()
157
158        # Draw our Scene
159        self.scene.draw()
160
161        # Activate the GUI camera before drawing GUI elements
162        self.gui_camera.use()
163
164        # Draw our score on the screen, scrolling it with the viewport
165        score_text = f"Score: {self.score}"
166        arcade.draw_text(
167            score_text,
168            10,
169            10,
170            arcade.csscolor.BLACK,
171            18,
172        )
173
174    def on_key_press(self, key, modifiers):
175        """Called whenever a key is pressed."""
176
177        if key == arcade.key.UP or key == arcade.key.W:
178            if self.physics_engine.can_jump():
179                self.player_sprite.change_y = PLAYER_JUMP_SPEED
180                arcade.play_sound(self.jump_sound)
181        elif key == arcade.key.LEFT or key == arcade.key.A:
182            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
183        elif key == arcade.key.RIGHT or key == arcade.key.D:
184            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
185
186    def on_key_release(self, key, modifiers):
187        """Called when the user releases a key."""
188
189        if key == arcade.key.LEFT or key == arcade.key.A:
190            self.player_sprite.change_x = 0
191        elif key == arcade.key.RIGHT or key == arcade.key.D:
192            self.player_sprite.change_x = 0
193
194    def center_camera_to_player(self):
195        screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2)
196        screen_center_y = self.player_sprite.center_y - (
197            self.camera.viewport_height / 2
198        )
199        if screen_center_x < 0:
200            screen_center_x = 0
201        if screen_center_y < 0:
202            screen_center_y = 0
203        player_centered = screen_center_x, screen_center_y
204
205        self.camera.move_to(player_centered)
206
207    def on_update(self, delta_time):
208        """Movement and game logic"""
209
210        # Move the player with the physics engine
211        self.physics_engine.update()
212
213        # See if we hit any coins
214        coin_hit_list = arcade.check_for_collision_with_list(
215            self.player_sprite, self.scene[LAYER_NAME_COINS]
216        )
217
218        # Loop through each coin we hit (if any) and remove it
219        for coin in coin_hit_list:
220            # Remove the coin
221            coin.remove_from_sprite_lists()
222            # Play a sound
223            arcade.play_sound(self.collect_coin_sound)
224            # Add one to the score
225            self.score += 1
226
227        # Did the player fall off the map?
228        if self.player_sprite.center_y < -100:
229            self.player_sprite.center_x = PLAYER_START_X
230            self.player_sprite.center_y = PLAYER_START_Y
231
232            arcade.play_sound(self.game_over)
233
234        # Did the player touch something they should not?
235        if arcade.check_for_collision_with_list(
236            self.player_sprite, self.scene[LAYER_NAME_DONT_TOUCH]
237        ):
238            self.player_sprite.change_x = 0
239            self.player_sprite.change_y = 0
240            self.player_sprite.center_x = PLAYER_START_X
241            self.player_sprite.center_y = PLAYER_START_Y
242
243            arcade.play_sound(self.game_over)
244
245        # See if the user got to the end of the level
246        if self.player_sprite.center_x >= self.end_of_map:
247            # Advance to the next level
248            self.level += 1
249
250            # Make sure to keep the score from this level when setting up the next level
251            self.reset_score = False
252
253            # Load the next level
254            self.setup()
255
256        # Position the camera
257        self.center_camera_to_player()
258
259
260def main():
261    """Main function"""
262    window = MyGame()
263    window.setup()
264    arcade.run()
265
266
267if __name__ == "__main__":
268    main()