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#
# Constants used to scale our sprites from their original size
CHARACTER_SCALING = 1

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_MOVEMENT_SPEED = 10
GRAVITY = 1
PLAYER_JUMP_SPEED = 20

# Player starting position
PLAYER_START_X = 64
PLAYER_START_Y = 225

# Layer Names from our TileMap
LAYER_NAME_PLATFORMS = "Platforms"

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#

        # Do we need to reset the score?
        self.reset_score = True

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

        # Level
        self.level = 1

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#

        # Keep track of the score
        self.score = 0

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#
        # Set up the Cameras
        viewport = (0, 0, self.width, self.height)
        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,

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#
        # Keep track of the score, make sure we keep the score if the player finishes a level
        if self.reset_score:
            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.

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)

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

Multiple Levels - Setup Function#
        self.player_sprite.center_y = PLAYER_START_Y
        self.scene.add_sprite("Player", self.player_sprite)

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#
            # Play a sound
            arcade.play_sound(self.collect_coin_sound)
            # 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

Source Code#

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