Step 13 - Add Enemies#
1"""
2Platformer Game
3
4python -m arcade.examples.platform_tutorial.13_add_enemies
5"""
6from __future__ import annotations
7
8import math
9
10import arcade
11
12# Constants
13SCREEN_WIDTH = 1000
14SCREEN_HEIGHT = 650
15SCREEN_TITLE = "Platformer"
16
17# Constants used to scale our sprites from their original size
18TILE_SCALING = 0.5
19CHARACTER_SCALING = TILE_SCALING * 2
20COIN_SCALING = TILE_SCALING
21SPRITE_PIXEL_SIZE = 128
22GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING
23
24# Movement speed of player, in pixels per frame
25PLAYER_MOVEMENT_SPEED = 7
26GRAVITY = 1.5
27PLAYER_JUMP_SPEED = 30
28
29PLAYER_START_X = 2
30PLAYER_START_Y = 1
31
32# Constants used to track if the player is facing left or right
33RIGHT_FACING = 0
34LEFT_FACING = 1
35
36LAYER_NAME_MOVING_PLATFORMS = "Moving Platforms"
37LAYER_NAME_PLATFORMS = "Platforms"
38LAYER_NAME_COINS = "Coins"
39LAYER_NAME_BACKGROUND = "Background"
40LAYER_NAME_LADDERS = "Ladders"
41LAYER_NAME_PLAYER = "Player"
42LAYER_NAME_ENEMIES = "Enemies"
43
44
45def load_texture_pair(filename):
46 """
47 Load a texture pair, with the second being a mirror image.
48 """
49 return [
50 arcade.load_texture(filename),
51 arcade.load_texture(filename, flipped_horizontally=True),
52 ]
53
54
55class Entity(arcade.Sprite):
56 def __init__(self, name_folder, name_file):
57 super().__init__()
58
59 # Default to facing right
60 self.facing_direction = RIGHT_FACING
61
62 # Used for image sequences
63 self.cur_texture = 0
64 self.scale = CHARACTER_SCALING
65 self.character_face_direction = RIGHT_FACING
66
67 main_path = f":resources:images/animated_characters/{name_folder}/{name_file}"
68
69 self.idle_texture_pair = load_texture_pair(f"{main_path}_idle.png")
70 self.jump_texture_pair = load_texture_pair(f"{main_path}_jump.png")
71 self.fall_texture_pair = load_texture_pair(f"{main_path}_fall.png")
72
73 # Load textures for walking
74 self.walk_textures = []
75 for i in range(8):
76 texture = load_texture_pair(f"{main_path}_walk{i}.png")
77 self.walk_textures.append(texture)
78
79 # Load textures for climbing
80 self.climbing_textures = []
81 texture = arcade.load_texture(f"{main_path}_climb0.png")
82 self.climbing_textures.append(texture)
83 texture = arcade.load_texture(f"{main_path}_climb1.png")
84 self.climbing_textures.append(texture)
85
86 # Set the initial texture
87 self.texture = self.idle_texture_pair[0]
88
89 # Hit box will be set based on the first image used. If you want to specify
90 # a different hit box, you can do it like the code below. Doing this when
91 # changing the texture for example would make the hitbox update whenever the
92 # texture is changed. This can be expensive so if the textures are very similar
93 # it may not be worth doing.
94 #
95 # self.hit_box = arcade.hitbox.RotatableHitBox(
96 # self.texture.hit_box_points,
97 # position=self.position,
98 # scale=self.scale_xy,
99 # angle=self.angle,
100 # )
101
102
103class Enemy(Entity):
104 def __init__(self, name_folder, name_file):
105 # Setup parent class
106 super().__init__(name_folder, name_file)
107
108
109class RobotEnemy(Enemy):
110 def __init__(self):
111 # Set up parent class
112 super().__init__("robot", "robot")
113
114
115class ZombieEnemy(Enemy):
116 def __init__(self):
117 # Set up parent class
118 super().__init__("zombie", "zombie")
119
120
121class PlayerCharacter(Entity):
122 """Player Sprite"""
123
124 def __init__(self):
125 # Set up parent class
126 super().__init__("male_person", "malePerson")
127
128 # Track our state
129 self.jumping = False
130 self.climbing = False
131 self.is_on_ladder = False
132
133 def update_animation(self, delta_time: float = 1 / 60):
134 # Figure out if we need to flip face left or right
135 if self.change_x < 0 and self.facing_direction == RIGHT_FACING:
136 self.facing_direction = LEFT_FACING
137 elif self.change_x > 0 and self.facing_direction == LEFT_FACING:
138 self.facing_direction = RIGHT_FACING
139
140 # Climbing animation
141 if self.is_on_ladder:
142 self.climbing = True
143 if not self.is_on_ladder and self.climbing:
144 self.climbing = False
145 if self.climbing and abs(self.change_y) > 1:
146 self.cur_texture += 1
147 if self.cur_texture > 7:
148 self.cur_texture = 0
149 if self.climbing:
150 self.texture = self.climbing_textures[self.cur_texture // 4]
151 return
152
153 # Jumping animation
154 if self.change_y > 0 and not self.is_on_ladder:
155 self.texture = self.jump_texture_pair[self.facing_direction]
156 return
157 elif self.change_y < 0 and not self.is_on_ladder:
158 self.texture = self.fall_texture_pair[self.facing_direction]
159 return
160
161 # Idle animation
162 if self.change_x == 0:
163 self.texture = self.idle_texture_pair[self.facing_direction]
164 return
165
166 # Walking animation
167 self.cur_texture += 1
168 if self.cur_texture > 7:
169 self.cur_texture = 0
170 self.texture = self.walk_textures[self.cur_texture][self.facing_direction]
171
172
173class MyGame(arcade.Window):
174 """
175 Main application class.
176 """
177
178 def __init__(self):
179 """
180 Initializer for the game
181 """
182 # Call the parent class and set up the window
183 super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
184
185 # Track the current state of what key is pressed
186 self.left_pressed = False
187 self.right_pressed = False
188 self.up_pressed = False
189 self.down_pressed = False
190 self.jump_needs_reset = False
191
192 # Our TileMap Object
193 self.tile_map = None
194
195 # Our Scene Object
196 self.scene = None
197
198 # Separate variable that holds the player sprite
199 self.player_sprite = None
200
201 # Our 'physics' engine
202 self.physics_engine = None
203
204 # A Camera that can be used for scrolling the screen
205 self.camera = None
206
207 # A Camera that can be used to draw GUI elements
208 self.gui_camera = None
209
210 self.end_of_map = 0
211
212 # Keep track of the score
213 self.score = 0
214
215 # Load sounds
216 self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
217 self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
218 self.game_over = arcade.load_sound(":resources:sounds/gameover1.wav")
219
220 def setup(self):
221 """Set up the game here. Call this function to restart the game."""
222
223 # Set up the Cameras
224 viewport = (0, 0, self.width, self.height)
225 self.camera = arcade.SimpleCamera(viewport=viewport)
226 self.gui_camera = arcade.SimpleCamera(viewport=viewport)
227
228 # Map name
229 map_name = ":resources:tiled_maps/map_with_ladders.json"
230
231 # Layer Specific Options for the Tilemap
232 layer_options = {
233 LAYER_NAME_PLATFORMS: {
234 "use_spatial_hash": True,
235 },
236 LAYER_NAME_MOVING_PLATFORMS: {
237 "use_spatial_hash": False,
238 },
239 LAYER_NAME_LADDERS: {
240 "use_spatial_hash": True,
241 },
242 LAYER_NAME_COINS: {
243 "use_spatial_hash": True,
244 },
245 }
246
247 # Load in TileMap
248 self.tile_map = arcade.load_tilemap(map_name, TILE_SCALING, layer_options)
249
250 # Initiate New Scene with our TileMap, this will automatically add all layers
251 # from the map as SpriteLists in the scene in the proper order.
252 self.scene = arcade.Scene.from_tilemap(self.tile_map)
253
254 # Keep track of the score
255 self.score = 0
256
257 # Set up the player, specifically placing it at these coordinates.
258 self.player_sprite = PlayerCharacter()
259 self.player_sprite.center_x = (
260 self.tile_map.tile_width * TILE_SCALING * PLAYER_START_X
261 )
262 self.player_sprite.center_y = (
263 self.tile_map.tile_height * TILE_SCALING * PLAYER_START_Y
264 )
265 self.scene.add_sprite(LAYER_NAME_PLAYER, self.player_sprite)
266
267 # Calculate the right edge of the my_map in pixels
268 self.end_of_map = self.tile_map.width * GRID_PIXEL_SIZE
269
270 # -- Enemies
271 enemies_layer = self.tile_map.object_lists[LAYER_NAME_ENEMIES]
272
273 for my_object in enemies_layer:
274 cartesian = self.tile_map.get_cartesian(
275 my_object.shape[0], my_object.shape[1]
276 )
277 enemy_type = my_object.properties["type"]
278 if enemy_type == "robot":
279 enemy = RobotEnemy()
280 elif enemy_type == "zombie":
281 enemy = ZombieEnemy()
282 else:
283 raise Exception(f"Unknown enemy type {enemy_type}.")
284 enemy.center_x = math.floor(
285 cartesian[0] * TILE_SCALING * self.tile_map.tile_width
286 )
287 enemy.center_y = math.floor(
288 (cartesian[1] + 1) * (self.tile_map.tile_height * TILE_SCALING)
289 )
290 self.scene.add_sprite(LAYER_NAME_ENEMIES, enemy)
291
292 # --- Other stuff
293 # Set the background color
294 if self.tile_map.background_color:
295 self.background_color = self.tile_map.background_color
296
297 # Create the 'physics engine'
298 self.physics_engine = arcade.PhysicsEnginePlatformer(
299 self.player_sprite,
300 platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS],
301 gravity_constant=GRAVITY,
302 ladders=self.scene[LAYER_NAME_LADDERS],
303 walls=self.scene[LAYER_NAME_PLATFORMS],
304 )
305
306 def on_draw(self):
307 """Render the screen."""
308
309 # Clear the screen to the background color
310 self.clear()
311
312 # Activate the game camera
313 self.camera.use()
314
315 # Draw our Scene
316 self.scene.draw()
317
318 # Activate the GUI camera before drawing GUI elements
319 self.gui_camera.use()
320
321 # Draw our score on the screen, scrolling it with the viewport
322 score_text = f"Score: {self.score}"
323 arcade.draw_text(
324 score_text,
325 10,
326 10,
327 arcade.csscolor.BLACK,
328 18,
329 )
330
331 def process_keychange(self):
332 """
333 Called when we change a key up/down or we move on/off a ladder.
334 """
335 # Process up/down
336 if self.up_pressed and not self.down_pressed:
337 if self.physics_engine.is_on_ladder():
338 self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
339 elif (
340 self.physics_engine.can_jump(y_distance=10)
341 and not self.jump_needs_reset
342 ):
343 self.player_sprite.change_y = PLAYER_JUMP_SPEED
344 self.jump_needs_reset = True
345 arcade.play_sound(self.jump_sound)
346 elif self.down_pressed and not self.up_pressed:
347 if self.physics_engine.is_on_ladder():
348 self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
349
350 # Process up/down when on a ladder and no movement
351 if self.physics_engine.is_on_ladder():
352 if not self.up_pressed and not self.down_pressed:
353 self.player_sprite.change_y = 0
354 elif self.up_pressed and self.down_pressed:
355 self.player_sprite.change_y = 0
356
357 # Process left/right
358 if self.right_pressed and not self.left_pressed:
359 self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
360 elif self.left_pressed and not self.right_pressed:
361 self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
362 else:
363 self.player_sprite.change_x = 0
364
365 def on_key_press(self, key, modifiers):
366 """Called whenever a key is pressed."""
367
368 if key == arcade.key.UP or key == arcade.key.W:
369 self.up_pressed = True
370 elif key == arcade.key.DOWN or key == arcade.key.S:
371 self.down_pressed = True
372 elif key == arcade.key.LEFT or key == arcade.key.A:
373 self.left_pressed = True
374 elif key == arcade.key.RIGHT or key == arcade.key.D:
375 self.right_pressed = True
376
377 self.process_keychange()
378
379 def on_key_release(self, key, modifiers):
380 """Called when the user releases a key."""
381
382 if key == arcade.key.UP or key == arcade.key.W:
383 self.up_pressed = False
384 self.jump_needs_reset = False
385 elif key == arcade.key.DOWN or key == arcade.key.S:
386 self.down_pressed = False
387 elif key == arcade.key.LEFT or key == arcade.key.A:
388 self.left_pressed = False
389 elif key == arcade.key.RIGHT or key == arcade.key.D:
390 self.right_pressed = False
391
392 self.process_keychange()
393
394 def center_camera_to_player(self):
395 screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2)
396 screen_center_y = self.player_sprite.center_y - (
397 self.camera.viewport_height / 2
398 )
399 if screen_center_x < 0:
400 screen_center_x = 0
401 if screen_center_y < 0:
402 screen_center_y = 0
403 player_centered = screen_center_x, screen_center_y
404
405 self.camera.move_to(player_centered, 0.2)
406
407 def on_update(self, delta_time):
408 """Movement and game logic"""
409
410 # Move the player with the physics engine
411 self.physics_engine.update()
412
413 # Update animations
414 if self.physics_engine.can_jump():
415 self.player_sprite.can_jump = False
416 else:
417 self.player_sprite.can_jump = True
418
419 if self.physics_engine.is_on_ladder() and not self.physics_engine.can_jump():
420 self.player_sprite.is_on_ladder = True
421 self.process_keychange()
422 else:
423 self.player_sprite.is_on_ladder = False
424 self.process_keychange()
425
426 # Update Animations
427 self.scene.update_animation(
428 delta_time,
429 [
430 LAYER_NAME_COINS,
431 LAYER_NAME_BACKGROUND,
432 LAYER_NAME_PLAYER,
433 LAYER_NAME_ENEMIES,
434 ],
435 )
436
437 # Update walls, used with moving platforms
438 self.scene.update([LAYER_NAME_MOVING_PLATFORMS])
439
440 # See if we hit any coins
441 coin_hit_list = arcade.check_for_collision_with_list(
442 self.player_sprite, self.scene[LAYER_NAME_COINS]
443 )
444
445 # Loop through each coin we hit (if any) and remove it
446 for coin in coin_hit_list:
447 # Figure out how many points this coin is worth
448 if "Points" not in coin.properties:
449 print("Warning, collected a coin without a Points property.")
450 else:
451 points = int(coin.properties["Points"])
452 self.score += points
453
454 # Remove the coin
455 coin.remove_from_sprite_lists()
456 arcade.play_sound(self.collect_coin_sound)
457
458 # Position the camera
459 self.center_camera_to_player()
460
461
462def main():
463 """Main function"""
464 window = MyGame()
465 window.setup()
466 arcade.run()
467
468
469if __name__ == "__main__":
470 main()
Source Code#
1"""
2Platformer Game
3
4python -m arcade.examples.platform_tutorial.13_add_enemies
5"""
6from __future__ import annotations
7
8import math
9
10import arcade
11
12# Constants
13SCREEN_WIDTH = 1000
14SCREEN_HEIGHT = 650
15SCREEN_TITLE = "Platformer"
16
17# Constants used to scale our sprites from their original size
18TILE_SCALING = 0.5
19CHARACTER_SCALING = TILE_SCALING * 2
20COIN_SCALING = TILE_SCALING
21SPRITE_PIXEL_SIZE = 128
22GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING
23
24# Movement speed of player, in pixels per frame
25PLAYER_MOVEMENT_SPEED = 7
26GRAVITY = 1.5
27PLAYER_JUMP_SPEED = 30
28
29PLAYER_START_X = 2
30PLAYER_START_Y = 1
31
32# Constants used to track if the player is facing left or right
33RIGHT_FACING = 0
34LEFT_FACING = 1
35
36LAYER_NAME_MOVING_PLATFORMS = "Moving Platforms"
37LAYER_NAME_PLATFORMS = "Platforms"
38LAYER_NAME_COINS = "Coins"
39LAYER_NAME_BACKGROUND = "Background"
40LAYER_NAME_LADDERS = "Ladders"
41LAYER_NAME_PLAYER = "Player"
42LAYER_NAME_ENEMIES = "Enemies"
43
44
45def load_texture_pair(filename):
46 """
47 Load a texture pair, with the second being a mirror image.
48 """
49 return [
50 arcade.load_texture(filename),
51 arcade.load_texture(filename, flipped_horizontally=True),
52 ]
53
54
55class Entity(arcade.Sprite):
56 def __init__(self, name_folder, name_file):
57 super().__init__()
58
59 # Default to facing right
60 self.facing_direction = RIGHT_FACING
61
62 # Used for image sequences
63 self.cur_texture = 0
64 self.scale = CHARACTER_SCALING
65 self.character_face_direction = RIGHT_FACING
66
67 main_path = f":resources:images/animated_characters/{name_folder}/{name_file}"
68
69 self.idle_texture_pair = load_texture_pair(f"{main_path}_idle.png")
70 self.jump_texture_pair = load_texture_pair(f"{main_path}_jump.png")
71 self.fall_texture_pair = load_texture_pair(f"{main_path}_fall.png")
72
73 # Load textures for walking
74 self.walk_textures = []
75 for i in range(8):
76 texture = load_texture_pair(f"{main_path}_walk{i}.png")
77 self.walk_textures.append(texture)
78
79 # Load textures for climbing
80 self.climbing_textures = []
81 texture = arcade.load_texture(f"{main_path}_climb0.png")
82 self.climbing_textures.append(texture)
83 texture = arcade.load_texture(f"{main_path}_climb1.png")
84 self.climbing_textures.append(texture)
85
86 # Set the initial texture
87 self.texture = self.idle_texture_pair[0]
88
89 # Hit box will be set based on the first image used. If you want to specify
90 # a different hit box, you can do it like the code below. Doing this when
91 # changing the texture for example would make the hitbox update whenever the
92 # texture is changed. This can be expensive so if the textures are very similar
93 # it may not be worth doing.
94 #
95 # self.hit_box = arcade.hitbox.RotatableHitBox(
96 # self.texture.hit_box_points,
97 # position=self.position,
98 # scale=self.scale_xy,
99 # angle=self.angle,
100 # )
101
102
103class Enemy(Entity):
104 def __init__(self, name_folder, name_file):
105 # Setup parent class
106 super().__init__(name_folder, name_file)
107
108
109class RobotEnemy(Enemy):
110 def __init__(self):
111 # Set up parent class
112 super().__init__("robot", "robot")
113
114
115class ZombieEnemy(Enemy):
116 def __init__(self):
117 # Set up parent class
118 super().__init__("zombie", "zombie")
119
120
121class PlayerCharacter(Entity):
122 """Player Sprite"""
123
124 def __init__(self):
125 # Set up parent class
126 super().__init__("male_person", "malePerson")
127
128 # Track our state
129 self.jumping = False
130 self.climbing = False
131 self.is_on_ladder = False
132
133 def update_animation(self, delta_time: float = 1 / 60):
134 # Figure out if we need to flip face left or right
135 if self.change_x < 0 and self.facing_direction == RIGHT_FACING:
136 self.facing_direction = LEFT_FACING
137 elif self.change_x > 0 and self.facing_direction == LEFT_FACING:
138 self.facing_direction = RIGHT_FACING
139
140 # Climbing animation
141 if self.is_on_ladder:
142 self.climbing = True
143 if not self.is_on_ladder and self.climbing:
144 self.climbing = False
145 if self.climbing and abs(self.change_y) > 1:
146 self.cur_texture += 1
147 if self.cur_texture > 7:
148 self.cur_texture = 0
149 if self.climbing:
150 self.texture = self.climbing_textures[self.cur_texture // 4]
151 return
152
153 # Jumping animation
154 if self.change_y > 0 and not self.is_on_ladder:
155 self.texture = self.jump_texture_pair[self.facing_direction]
156 return
157 elif self.change_y < 0 and not self.is_on_ladder:
158 self.texture = self.fall_texture_pair[self.facing_direction]
159 return
160
161 # Idle animation
162 if self.change_x == 0:
163 self.texture = self.idle_texture_pair[self.facing_direction]
164 return
165
166 # Walking animation
167 self.cur_texture += 1
168 if self.cur_texture > 7:
169 self.cur_texture = 0
170 self.texture = self.walk_textures[self.cur_texture][self.facing_direction]
171
172
173class MyGame(arcade.Window):
174 """
175 Main application class.
176 """
177
178 def __init__(self):
179 """
180 Initializer for the game
181 """
182 # Call the parent class and set up the window
183 super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
184
185 # Track the current state of what key is pressed
186 self.left_pressed = False
187 self.right_pressed = False
188 self.up_pressed = False
189 self.down_pressed = False
190 self.jump_needs_reset = False
191
192 # Our TileMap Object
193 self.tile_map = None
194
195 # Our Scene Object
196 self.scene = None
197
198 # Separate variable that holds the player sprite
199 self.player_sprite = None
200
201 # Our 'physics' engine
202 self.physics_engine = None
203
204 # A Camera that can be used for scrolling the screen
205 self.camera = None
206
207 # A Camera that can be used to draw GUI elements
208 self.gui_camera = None
209
210 self.end_of_map = 0
211
212 # Keep track of the score
213 self.score = 0
214
215 # Load sounds
216 self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
217 self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
218 self.game_over = arcade.load_sound(":resources:sounds/gameover1.wav")
219
220 def setup(self):
221 """Set up the game here. Call this function to restart the game."""
222
223 # Set up the Cameras
224 viewport = (0, 0, self.width, self.height)
225 self.camera = arcade.SimpleCamera(viewport=viewport)
226 self.gui_camera = arcade.SimpleCamera(viewport=viewport)
227
228 # Map name
229 map_name = ":resources:tiled_maps/map_with_ladders.json"
230
231 # Layer Specific Options for the Tilemap
232 layer_options = {
233 LAYER_NAME_PLATFORMS: {
234 "use_spatial_hash": True,
235 },
236 LAYER_NAME_MOVING_PLATFORMS: {
237 "use_spatial_hash": False,
238 },
239 LAYER_NAME_LADDERS: {
240 "use_spatial_hash": True,
241 },
242 LAYER_NAME_COINS: {
243 "use_spatial_hash": True,
244 },
245 }
246
247 # Load in TileMap
248 self.tile_map = arcade.load_tilemap(map_name, TILE_SCALING, layer_options)
249
250 # Initiate New Scene with our TileMap, this will automatically add all layers
251 # from the map as SpriteLists in the scene in the proper order.
252 self.scene = arcade.Scene.from_tilemap(self.tile_map)
253
254 # Keep track of the score
255 self.score = 0
256
257 # Set up the player, specifically placing it at these coordinates.
258 self.player_sprite = PlayerCharacter()
259 self.player_sprite.center_x = (
260 self.tile_map.tile_width * TILE_SCALING * PLAYER_START_X
261 )
262 self.player_sprite.center_y = (
263 self.tile_map.tile_height * TILE_SCALING * PLAYER_START_Y
264 )
265 self.scene.add_sprite(LAYER_NAME_PLAYER, self.player_sprite)
266
267 # Calculate the right edge of the my_map in pixels
268 self.end_of_map = self.tile_map.width * GRID_PIXEL_SIZE
269
270 # -- Enemies
271 enemies_layer = self.tile_map.object_lists[LAYER_NAME_ENEMIES]
272
273 for my_object in enemies_layer:
274 cartesian = self.tile_map.get_cartesian(
275 my_object.shape[0], my_object.shape[1]
276 )
277 enemy_type = my_object.properties["type"]
278 if enemy_type == "robot":
279 enemy = RobotEnemy()
280 elif enemy_type == "zombie":
281 enemy = ZombieEnemy()
282 else:
283 raise Exception(f"Unknown enemy type {enemy_type}.")
284 enemy.center_x = math.floor(
285 cartesian[0] * TILE_SCALING * self.tile_map.tile_width
286 )
287 enemy.center_y = math.floor(
288 (cartesian[1] + 1) * (self.tile_map.tile_height * TILE_SCALING)
289 )
290 self.scene.add_sprite(LAYER_NAME_ENEMIES, enemy)
291
292 # --- Other stuff
293 # Set the background color
294 if self.tile_map.background_color:
295 self.background_color = self.tile_map.background_color
296
297 # Create the 'physics engine'
298 self.physics_engine = arcade.PhysicsEnginePlatformer(
299 self.player_sprite,
300 platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS],
301 gravity_constant=GRAVITY,
302 ladders=self.scene[LAYER_NAME_LADDERS],
303 walls=self.scene[LAYER_NAME_PLATFORMS],
304 )
305
306 def on_draw(self):
307 """Render the screen."""
308
309 # Clear the screen to the background color
310 self.clear()
311
312 # Activate the game camera
313 self.camera.use()
314
315 # Draw our Scene
316 self.scene.draw()
317
318 # Activate the GUI camera before drawing GUI elements
319 self.gui_camera.use()
320
321 # Draw our score on the screen, scrolling it with the viewport
322 score_text = f"Score: {self.score}"
323 arcade.draw_text(
324 score_text,
325 10,
326 10,
327 arcade.csscolor.BLACK,
328 18,
329 )
330
331 def process_keychange(self):
332 """
333 Called when we change a key up/down or we move on/off a ladder.
334 """
335 # Process up/down
336 if self.up_pressed and not self.down_pressed:
337 if self.physics_engine.is_on_ladder():
338 self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
339 elif (
340 self.physics_engine.can_jump(y_distance=10)
341 and not self.jump_needs_reset
342 ):
343 self.player_sprite.change_y = PLAYER_JUMP_SPEED
344 self.jump_needs_reset = True
345 arcade.play_sound(self.jump_sound)
346 elif self.down_pressed and not self.up_pressed:
347 if self.physics_engine.is_on_ladder():
348 self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
349
350 # Process up/down when on a ladder and no movement
351 if self.physics_engine.is_on_ladder():
352 if not self.up_pressed and not self.down_pressed:
353 self.player_sprite.change_y = 0
354 elif self.up_pressed and self.down_pressed:
355 self.player_sprite.change_y = 0
356
357 # Process left/right
358 if self.right_pressed and not self.left_pressed:
359 self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
360 elif self.left_pressed and not self.right_pressed:
361 self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
362 else:
363 self.player_sprite.change_x = 0
364
365 def on_key_press(self, key, modifiers):
366 """Called whenever a key is pressed."""
367
368 if key == arcade.key.UP or key == arcade.key.W:
369 self.up_pressed = True
370 elif key == arcade.key.DOWN or key == arcade.key.S:
371 self.down_pressed = True
372 elif key == arcade.key.LEFT or key == arcade.key.A:
373 self.left_pressed = True
374 elif key == arcade.key.RIGHT or key == arcade.key.D:
375 self.right_pressed = True
376
377 self.process_keychange()
378
379 def on_key_release(self, key, modifiers):
380 """Called when the user releases a key."""
381
382 if key == arcade.key.UP or key == arcade.key.W:
383 self.up_pressed = False
384 self.jump_needs_reset = False
385 elif key == arcade.key.DOWN or key == arcade.key.S:
386 self.down_pressed = False
387 elif key == arcade.key.LEFT or key == arcade.key.A:
388 self.left_pressed = False
389 elif key == arcade.key.RIGHT or key == arcade.key.D:
390 self.right_pressed = False
391
392 self.process_keychange()
393
394 def center_camera_to_player(self):
395 screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2)
396 screen_center_y = self.player_sprite.center_y - (
397 self.camera.viewport_height / 2
398 )
399 if screen_center_x < 0:
400 screen_center_x = 0
401 if screen_center_y < 0:
402 screen_center_y = 0
403 player_centered = screen_center_x, screen_center_y
404
405 self.camera.move_to(player_centered, 0.2)
406
407 def on_update(self, delta_time):
408 """Movement and game logic"""
409
410 # Move the player with the physics engine
411 self.physics_engine.update()
412
413 # Update animations
414 if self.physics_engine.can_jump():
415 self.player_sprite.can_jump = False
416 else:
417 self.player_sprite.can_jump = True
418
419 if self.physics_engine.is_on_ladder() and not self.physics_engine.can_jump():
420 self.player_sprite.is_on_ladder = True
421 self.process_keychange()
422 else:
423 self.player_sprite.is_on_ladder = False
424 self.process_keychange()
425
426 # Update Animations
427 self.scene.update_animation(
428 delta_time,
429 [
430 LAYER_NAME_COINS,
431 LAYER_NAME_BACKGROUND,
432 LAYER_NAME_PLAYER,
433 LAYER_NAME_ENEMIES,
434 ],
435 )
436
437 # Update walls, used with moving platforms
438 self.scene.update([LAYER_NAME_MOVING_PLATFORMS])
439
440 # See if we hit any coins
441 coin_hit_list = arcade.check_for_collision_with_list(
442 self.player_sprite, self.scene[LAYER_NAME_COINS]
443 )
444
445 # Loop through each coin we hit (if any) and remove it
446 for coin in coin_hit_list:
447 # Figure out how many points this coin is worth
448 if "Points" not in coin.properties:
449 print("Warning, collected a coin without a Points property.")
450 else:
451 points = int(coin.properties["Points"])
452 self.score += points
453
454 # Remove the coin
455 coin.remove_from_sprite_lists()
456 arcade.play_sound(self.collect_coin_sound)
457
458 # Position the camera
459 self.center_camera_to_player()
460
461
462def main():
463 """Main function"""
464 window = MyGame()
465 window.setup()
466 arcade.run()
467
468
469if __name__ == "__main__":
470 main()