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