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#
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()