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