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
  9WINDOW_WIDTH = 1280
 10WINDOW_HEIGHT = 720
 11WINDOW_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 GameView(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__(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_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(
 81            f":resources:tiled_maps/map2_level_{self.level}.json",
 82            scaling=TILE_SCALING,
 83            layer_options=layer_options,
 84        )
 85
 86        # Create our Scene Based on the TileMap
 87        self.scene = arcade.Scene.from_tilemap(self.tile_map)
 88
 89        self.player_texture = arcade.load_texture(
 90            ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
 91        )
 92
 93        # Add Player Spritelist before "Foreground" layer. This will make the foreground
 94        # be drawn after the player, making it appear to be in front of the Player.
 95        # Setting before using scene.add_sprite allows us to define where the SpriteList
 96        # will be in the draw order. If we just use add_sprite, it will be appended to the
 97        # end of the order.
 98        self.scene.add_sprite_list_after("Player", "Foreground")
 99
100        self.player_sprite = arcade.Sprite(self.player_texture)
101        self.player_sprite.center_x = 128
102        self.player_sprite.center_y = 128
103        self.scene.add_sprite("Player", self.player_sprite)
104
105        # Create a Platformer Physics Engine, this will handle moving our
106        # player as well as collisions between the player sprite and
107        # whatever SpriteList we specify for the walls.
108        # It is important to supply static to the walls parameter. There is a
109        # platforms parameter that is intended for moving platforms.
110        # If a platform is supposed to move, and is added to the walls list,
111        # it will not be moved.
112        self.physics_engine = arcade.PhysicsEnginePlatformer(
113            self.player_sprite, walls=self.scene["Platforms"], gravity_constant=GRAVITY
114        )
115
116        # Initialize our camera, setting a viewport the size of our window.
117        self.camera = arcade.camera.Camera2D()
118
119        # Initialize our gui camera, initial settings are the same as our world camera.
120        self.gui_camera = arcade.camera.Camera2D()
121
122        # Reset the score if we should
123        if self.reset_score:
124            self.score = 0
125        self.reset_score = True
126
127        # Initialize our arcade.Text object for score
128        self.score_text = arcade.Text(f"Score: {self.score}", x=0, y=5)
129
130        self.background_color = arcade.csscolor.CORNFLOWER_BLUE
131
132        # Calculate the right edge of the map in pixels
133        self.end_of_map = (self.tile_map.width * self.tile_map.tile_width)
134        self.end_of_map *= self.tile_map.scaling
135        print(self.end_of_map)
136
137    def on_draw(self):
138        """Render the screen."""
139
140        # Clear the screen to the background color
141        self.clear()
142
143        # Activate our camera before drawing
144        self.camera.use()
145
146        # Draw our Scene
147        self.scene.draw()
148
149        # Activate our GUI camera
150        self.gui_camera.use()
151
152        # Draw our Score
153        self.score_text.draw()
154
155    def on_update(self, delta_time):
156        """Movement and Game Logic"""
157
158        # Move the player using our physics engine
159        self.physics_engine.update()
160
161        # See if we hit any coins
162        coin_hit_list = arcade.check_for_collision_with_list(
163            self.player_sprite, self.scene["Coins"]
164        )
165
166        # Loop through each coin we hit (if any) and remove it
167        for coin in coin_hit_list:
168            # Remove the coin
169            coin.remove_from_sprite_lists()
170            arcade.play_sound(self.collect_coin_sound)
171            self.score += 75
172            self.score_text.text = f"Score: {self.score}"
173
174        if arcade.check_for_collision_with_list(
175            self.player_sprite, self.scene["Don't Touch"]
176        ):
177            arcade.play_sound(self.gameover_sound)
178            self.setup()
179
180        # Check if the player got to the end of the level
181        if self.player_sprite.center_x >= self.end_of_map:
182            # Advance to the next level
183            self.level += 1
184
185            # Turn off score reset when advancing level
186            self.reset_score = False
187
188            # Reload game with new level
189            self.setup()
190
191        # Center our camera on the player
192        self.camera.position = self.player_sprite.position
193
194    def on_key_press(self, key, modifiers):
195        """Called whenever a key is pressed."""
196
197        if key == arcade.key.ESCAPE:
198            self.setup()
199
200        if key == arcade.key.UP or key == arcade.key.W:
201            if self.physics_engine.can_jump():
202                self.player_sprite.change_y = PLAYER_JUMP_SPEED
203                arcade.play_sound(self.jump_sound)
204
205        if key == arcade.key.LEFT or key == arcade.key.A:
206            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
207        elif key == arcade.key.RIGHT or key == arcade.key.D:
208            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
209
210    def on_key_release(self, key, modifiers):
211        """Called whenever a key is released."""
212
213        if key == arcade.key.LEFT or key == arcade.key.A:
214            self.player_sprite.change_x = 0
215        elif key == arcade.key.RIGHT or key == arcade.key.D:
216            self.player_sprite.change_x = 0
217
218
219def main():
220    """Main function"""
221    window = GameView()
222    window.setup()
223    arcade.run()
224
225
226if __name__ == "__main__":
227    main()