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