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