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