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