Step 14 - Multiple Levels#

Now we will make it so that our game has multiple levels. For now we will just have two levels, but this technique can be easily expanded to include more.

To start off, create two new variables in the __init__ function to represent the position that marks the end of the map, and what level we should be loading.

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

# Level number to load
self.level = 1

Next in the setup function we will change the map loading call to use an f-string to load a map file depending on the level variable we created.

# Load our TileMap
self.tile_map = arcade.load_tilemap(f":resources:tiled_maps/map2_level_{self.level}.json", scaling=TILE_SCALING, layer_options=layer_options)

Again in the setup function, we will calculate where the edge of the currently loaded map is, in pixels. To do this we get the width of the map, which is represented in number of tiles, and multiply it by the tile width. We also need to consider the scaling of the tiles, because we are measuring this in pixels.

# Calculate the right edge of the map in pixels
self.end_of_map = (self.tile_map.width * self.tile_map.tile_width) * self.tile_map.scaling

Now in the on_update function, we will add a block to check the player position against the end of the map value. We will do this right before the center_camera_to_player function call at the end. This will increment our current level, and leverage the setup function in order to re-load the game with the new level.

# Check if the player 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

    # Reload game with new level
    self.setup()

If you run the game at this point, you will be able to reach the end of the first level and have the next level load and play through it. We have two problems at this point, did you notice them? The first problem is that the player’s score resets in between levels, maybe you want this to happen in your game, but we will fix it here so that when switching levels we don’t reset the score.

To do this, first add a new variable to the __init__ function which will serve as a trigger to know if the score should be reset or not. We want to be able to reset it when the player loses, so this trigger will help us only reset the score when we want to.

# Should we reset the score?
self.reset_score = True

Now in the setup function we can replace the score reset with this block of code. We change the reset_score variable back to True after resetting the score, because the default in our game should be to reset it, and we only turn off the reset when we want it off.

# Reset the score if we should
if self.reset_score:
    self.score = 0
self.reset_score = True

Finally, in the section of on_update that we advance the level, we can add this line to turn off the score reset

# Turn off score reset when advancing level
self.reset_score = False

Now the player’s score will persist between levels, but we still have one more problem. If you reach the end of the second level, the game crashes! This is because we only actually have two levels available, but we are still trying to advance the level to 3 when we hit the end of level 2.

There’s a few ways this can be handled, one way is to simply make more levels. Eventually you have to have a final level though, so this probably isn’t the best solution. As an exercise, see if you can find a way to gracefully handle the final level. You could display an end screen, or restart the game from the beginning, or anything you want.

Source Code#

Moving the enemies#
  1"""
  2Platformer Game
  3
  4python -m arcade.examples.platform_tutorial.14_multiple_levels
  5"""
  6import arcade
  7
  8# Constants
  9SCREEN_WIDTH = 800
 10SCREEN_HEIGHT = 600
 11SCREEN_TITLE = "Platformer"
 12
 13# Constants used to scale our sprites from their original size
 14TILE_SCALING = 0.5
 15COIN_SCALING = 0.5
 16
 17# Movement speed of player, in pixels per frame
 18PLAYER_MOVEMENT_SPEED = 5
 19GRAVITY = 1
 20PLAYER_JUMP_SPEED = 20
 21
 22
 23class MyGame(arcade.Window):
 24    """
 25    Main application class.
 26    """
 27
 28    def __init__(self):
 29
 30        # Call the parent class and set up the window
 31        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 32
 33        # Variable to hold our texture for our player
 34        self.player_texture = None
 35
 36        # Separate variable that holds the player sprite
 37        self.player_sprite = None
 38
 39        # Variable to hold our Tiled Map
 40        self.tile_map = None
 41
 42        # Replacing all of our SpriteLists with a Scene variable
 43        self.scene = None
 44
 45        # A variable to store our camera object
 46        self.camera = None
 47
 48        # A variable to store our gui camera object
 49        self.gui_camera = None
 50
 51        # This variable will store our score as an integer.
 52        self.score = 0
 53
 54        # This variable will store the text for score that we will draw to the screen.
 55        self.score_text = None
 56
 57        # Where is the right edge of the map?
 58        self.end_of_map = 0
 59
 60        # Level number to load
 61        self.level = 1
 62
 63        # Should we reset the score?
 64        self.reset_score = True
 65
 66        # Load sounds
 67        self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
 68        self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
 69        self.gameover_sound = arcade.load_sound(":resources:sounds/gameover1.wav")
 70
 71    def setup(self):
 72        """Set up the game here. Call this function to restart the game."""
 73        layer_options = {
 74            "Platforms": {
 75                "use_spatial_hash": True
 76            }
 77        }
 78
 79        # Load our TileMap
 80        self.tile_map = arcade.load_tilemap(f":resources:tiled_maps/map2_level_{self.level}.json", scaling=TILE_SCALING, layer_options=layer_options)
 81
 82        # Create our Scene Based on the TileMap
 83        self.scene = arcade.Scene.from_tilemap(self.tile_map)
 84
 85        self.player_texture = arcade.load_texture(":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png")
 86
 87        # Add Player Spritelist before "Foreground" layer. This will make the foreground
 88        # be drawn after the player, making it appear to be in front of the Player.
 89        # Setting before using scene.add_sprite allows us to define where the SpriteList
 90        # will be in the draw order. If we just use add_sprite, it will be appended to the
 91        # end of the order.
 92        self.scene.add_sprite_list_after("Player", "Foreground")
 93
 94        self.player_sprite = arcade.Sprite(self.player_texture)
 95        self.player_sprite.center_x = 128
 96        self.player_sprite.center_y = 128
 97        self.scene.add_sprite("Player", self.player_sprite)
 98
 99        # Create a Platformer Physics Engine, this will handle moving our
100        # player as well as collisions between the player sprite and
101        # whatever SpriteList we specify for the walls.
102        # It is important to supply static to the walls parameter. There is a
103        # platforms parameter that is intended for moving platforms.
104        # If a platform is supposed to move, and is added to the walls list,
105        # it will not be moved.
106        self.physics_engine = arcade.PhysicsEnginePlatformer(
107            self.player_sprite, walls=self.scene["Platforms"], gravity_constant=GRAVITY
108        )
109
110        # Initialize our camera, setting a viewport the size of our window.
111        self.camera = arcade.camera.Camera2D()
112
113        # Initialize our gui camera, initial settings are the same as our world camera.
114        self.gui_camera = arcade.camera.Camera2D()
115
116        # Reset the score if we should
117        if self.reset_score:
118            self.score = 0
119        self.reset_score = True
120
121        # Initialize our arcade.Text object for score
122        self.score_text = arcade.Text(f"Score: {self.score}", x=0, y=5)
123
124        self.background_color = arcade.csscolor.CORNFLOWER_BLUE
125
126        # Calculate the right edge of the map in pixels
127        self.end_of_map = (self.tile_map.width * self.tile_map.tile_width) * self.tile_map.scaling
128        print(self.end_of_map)
129
130    def on_draw(self):
131        """Render the screen."""
132
133        # Clear the screen to the background color
134        self.clear()
135
136        # Activate our camera before drawing
137        self.camera.use()
138
139        # Draw our Scene
140        self.scene.draw()
141
142        # Activate our GUI camera
143        self.gui_camera.use()
144
145        # Draw our Score
146        self.score_text.draw()
147
148    def on_update(self, delta_time):
149        """Movement and Game Logic"""
150
151        # Move the player using our physics engine
152        self.physics_engine.update()
153
154        # See if we hit any coins
155        coin_hit_list = arcade.check_for_collision_with_list(
156            self.player_sprite, self.scene["Coins"]
157        )
158
159        # Loop through each coin we hit (if any) and remove it
160        for coin in coin_hit_list:
161            # Remove the coin
162            coin.remove_from_sprite_lists()
163            arcade.play_sound(self.collect_coin_sound)
164            self.score += 75
165            self.score_text.text = f"Score: {self.score}"
166
167        if arcade.check_for_collision_with_list(
168            self.player_sprite, self.scene["Don't Touch"]
169        ):
170            arcade.play_sound(self.gameover_sound)
171            self.setup()
172
173        # Check if the player got to the end of the level
174        if self.player_sprite.center_x >= self.end_of_map:
175            # Advance to the next level
176            self.level += 1
177
178            # Turn off score reset when advancing level
179            self.reset_score = False
180
181            # Reload game with new level
182            self.setup()
183
184        # Center our camera on the player
185        self.camera.position = self.player_sprite.position
186
187    def on_key_press(self, key, modifiers):
188        """Called whenever a key is pressed."""
189
190        if key == arcade.key.ESCAPE:
191            self.setup()
192
193        if key == arcade.key.UP or key == arcade.key.W:
194            if self.physics_engine.can_jump():
195                self.player_sprite.change_y = PLAYER_JUMP_SPEED
196                arcade.play_sound(self.jump_sound)
197
198        if key == arcade.key.LEFT or key == arcade.key.A:
199            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
200        elif key == arcade.key.RIGHT or key == arcade.key.D:
201            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
202
203    def on_key_release(self, key, modifiers):
204        """Called whenever a key is released."""
205
206        if key == arcade.key.LEFT or key == arcade.key.A:
207            self.player_sprite.change_x = 0
208        elif key == arcade.key.RIGHT or key == arcade.key.D:
209            self.player_sprite.change_x = 0
210
211
212def main():
213    """Main function"""
214    window = MyGame()
215    window.setup()
216    arcade.run()
217
218
219if __name__ == "__main__":
220    main()