Step 17 - Views#
Shooting Bullets#
1"""
2Platformer Game
3
4python -m arcade.examples.platform_tutorial.17_views
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# Shooting Constants
23SPRITE_SCALING_LASER = 0.8
24SHOOT_SPEED = 15
25BULLET_SPEED = 12
26BULLET_DAMAGE = 25
27
28# Movement speed of player, in pixels per frame
29PLAYER_MOVEMENT_SPEED = 7
30GRAVITY = 1.5
31PLAYER_JUMP_SPEED = 30
32
33# How many pixels to keep as a minimum margin between the character
34# and the edge of the screen.
35LEFT_VIEWPORT_MARGIN = 200
36RIGHT_VIEWPORT_MARGIN = 200
37BOTTOM_VIEWPORT_MARGIN = 150
38TOP_VIEWPORT_MARGIN = 100
39
40PLAYER_START_X = 2
41PLAYER_START_Y = 1
42
43# Constants used to track if the player is facing left or right
44RIGHT_FACING = 0
45LEFT_FACING = 1
46
47LAYER_NAME_MOVING_PLATFORMS = "Moving Platforms"
48LAYER_NAME_PLATFORMS = "Platforms"
49LAYER_NAME_COINS = "Coins"
50LAYER_NAME_BACKGROUND = "Background"
51LAYER_NAME_LADDERS = "Ladders"
52LAYER_NAME_PLAYER = "Player"
53LAYER_NAME_ENEMIES = "Enemies"
54LAYER_NAME_BULLETS = "Bullets"
55
56
57def load_texture_pair(filename):
58 """
59 Load a texture pair, with the second being a mirror image.
60 """
61 return [
62 arcade.load_texture(filename),
63 arcade.load_texture(filename, flipped_horizontally=True),
64 ]
65
66
67class Entity(arcade.Sprite):
68 def __init__(self, name_folder, name_file):
69 super().__init__()
70
71 # Default to facing right
72 self.facing_direction = RIGHT_FACING
73
74 # Used for image sequences
75 self.cur_texture = 0
76 self.scale = CHARACTER_SCALING
77
78 main_path = f":resources:images/animated_characters/{name_folder}/{name_file}"
79
80 self.idle_texture_pair = load_texture_pair(f"{main_path}_idle.png")
81 self.jump_texture_pair = load_texture_pair(f"{main_path}_jump.png")
82 self.fall_texture_pair = load_texture_pair(f"{main_path}_fall.png")
83
84 # Load textures for walking
85 self.walk_textures = []
86 for i in range(8):
87 texture = load_texture_pair(f"{main_path}_walk{i}.png")
88 self.walk_textures.append(texture)
89
90 # Load textures for climbing
91 self.climbing_textures = []
92 texture = arcade.load_texture(f"{main_path}_climb0.png")
93 self.climbing_textures.append(texture)
94 texture = arcade.load_texture(f"{main_path}_climb1.png")
95 self.climbing_textures.append(texture)
96
97 # Set the initial texture
98 self.texture = self.idle_texture_pair[0]
99
100 # Hit box will be set based on the first image used. If you want to specify
101 # a different hit box, you can do it like the code below.
102 # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]])
103 self.set_hit_box(self.texture.hit_box_points)
104
105
106class Enemy(Entity):
107 def __init__(self, name_folder, name_file):
108
109 # Setup parent class
110 super().__init__(name_folder, name_file)
111
112 self.should_update_walk = 0
113 self.health = 0
114
115 def update_animation(self, delta_time: float = 1 / 60):
116
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
143 # Set up parent class
144 super().__init__("robot", "robot")
145
146 self.health = 100
147
148
149class ZombieEnemy(Enemy):
150 def __init__(self):
151
152 # Set up parent class
153 super().__init__("zombie", "zombie")
154
155 self.health = 50
156
157
158class PlayerCharacter(Entity):
159 """Player Sprite"""
160
161 def __init__(self):
162
163 # Set up parent class
164 super().__init__("male_person", "malePerson")
165
166 # Track our state
167 self.jumping = False
168 self.climbing = False
169 self.is_on_ladder = False
170
171 def update_animation(self, delta_time: float = 1 / 60):
172
173 # Figure out if we need to flip face left or right
174 if self.change_x < 0 and self.facing_direction == RIGHT_FACING:
175 self.facing_direction = LEFT_FACING
176 elif self.change_x > 0 and self.facing_direction == LEFT_FACING:
177 self.facing_direction = RIGHT_FACING
178
179 # Climbing animation
180 if self.is_on_ladder:
181 self.climbing = True
182 if not self.is_on_ladder and self.climbing:
183 self.climbing = False
184 if self.climbing and abs(self.change_y) > 1:
185 self.cur_texture += 1
186 if self.cur_texture > 7:
187 self.cur_texture = 0
188 if self.climbing:
189 self.texture = self.climbing_textures[self.cur_texture // 4]
190 return
191
192 # Jumping animation
193 if self.change_y > 0 and not self.is_on_ladder:
194 self.texture = self.jump_texture_pair[self.facing_direction]
195 return
196 elif self.change_y < 0 and not self.is_on_ladder:
197 self.texture = self.fall_texture_pair[self.facing_direction]
198 return
199
200 # Idle animation
201 if self.change_x == 0:
202 self.texture = self.idle_texture_pair[self.facing_direction]
203 return
204
205 # Walking animation
206 self.cur_texture += 1
207 if self.cur_texture > 7:
208 self.cur_texture = 0
209 self.texture = self.walk_textures[self.cur_texture][self.facing_direction]
210
211
212class MainMenu(arcade.View):
213 """Class that manages the 'menu' view."""
214
215 def on_show_view(self):
216 """Called when switching to this view."""
217 arcade.set_background_color(arcade.color.WHITE)
218
219 def on_draw(self):
220 """Draw the menu"""
221 self.clear()
222 arcade.draw_text(
223 "Main Menu - Click to play",
224 SCREEN_WIDTH / 2,
225 SCREEN_HEIGHT / 2,
226 arcade.color.BLACK,
227 font_size=30,
228 anchor_x="center",
229 )
230
231 def on_mouse_press(self, _x, _y, _button, _modifiers):
232 """Use a mouse press to advance to the 'game' view."""
233 game_view = GameView()
234 self.window.show_view(game_view)
235
236
237class GameView(arcade.View):
238 """
239 Main application class.
240 """
241
242 def __init__(self):
243 """
244 Initializer for the game
245 """
246 super().__init__()
247
248 # Track the current state of what key is pressed
249 self.left_pressed = False
250 self.right_pressed = False
251 self.up_pressed = False
252 self.down_pressed = False
253 self.shoot_pressed = False
254 self.jump_needs_reset = False
255
256 # Our TileMap Object
257 self.tile_map = None
258
259 # Our Scene Object
260 self.scene = None
261
262 # Separate variable that holds the player sprite
263 self.player_sprite = None
264
265 # Our 'physics' engine
266 self.physics_engine = None
267
268 # A Camera that can be used for scrolling the screen
269 self.camera = None
270
271 # A Camera that can be used to draw GUI elements
272 self.gui_camera = None
273
274 self.end_of_map = 0
275
276 # Keep track of the score
277 self.score = 0
278
279 # Shooting mechanics
280 self.can_shoot = False
281 self.shoot_timer = 0
282
283 # Load sounds
284 self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
285 self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
286 self.game_over = arcade.load_sound(":resources:sounds/gameover1.wav")
287 self.shoot_sound = arcade.load_sound(":resources:sounds/hurt5.wav")
288 self.hit_sound = arcade.load_sound(":resources:sounds/hit5.wav")
289
290 def setup(self):
291 """Set up the game here. Call this function to restart the game."""
292
293 # Set up the Cameras
294 viewport = (0, 0, self.window.width, self.window.height)
295 self.camera = arcade.SimpleCamera(viewport=viewport)
296 self.gui_camera = arcade.SimpleCamera(viewport=viewport)
297
298 # Map name
299 map_name = ":resources:tiled_maps/map_with_ladders.json"
300
301 # Layer Specific Options for the Tilemap
302 layer_options = {
303 LAYER_NAME_PLATFORMS: {
304 "use_spatial_hash": True,
305 },
306 LAYER_NAME_MOVING_PLATFORMS: {
307 "use_spatial_hash": False,
308 },
309 LAYER_NAME_LADDERS: {
310 "use_spatial_hash": True,
311 },
312 LAYER_NAME_COINS: {
313 "use_spatial_hash": True,
314 },
315 }
316
317 # Load in TileMap
318 self.tile_map = arcade.load_tilemap(map_name, TILE_SCALING, layer_options)
319
320 # Initiate New Scene with our TileMap, this will automatically add all layers
321 # from the map as SpriteLists in the scene in the proper order.
322 self.scene = arcade.Scene.from_tilemap(self.tile_map)
323
324 # Keep track of the score
325 self.score = 0
326
327 # Shooting mechanics
328 self.can_shoot = True
329 self.shoot_timer = 0
330
331 # Set up the player, specifically placing it at these coordinates.
332 self.player_sprite = PlayerCharacter()
333 self.player_sprite.center_x = (
334 self.tile_map.tile_width * TILE_SCALING * PLAYER_START_X
335 )
336 self.player_sprite.center_y = (
337 self.tile_map.tile_height * TILE_SCALING * PLAYER_START_Y
338 )
339 self.scene.add_sprite(LAYER_NAME_PLAYER, self.player_sprite)
340
341 # Calculate the right edge of the my_map in pixels
342 self.end_of_map = self.tile_map.width * GRID_PIXEL_SIZE
343
344 # -- Enemies
345 enemies_layer = self.tile_map.object_lists[LAYER_NAME_ENEMIES]
346
347 for my_object in enemies_layer:
348 cartesian = self.tile_map.get_cartesian(
349 my_object.shape[0], my_object.shape[1]
350 )
351 enemy_type = my_object.properties["type"]
352 if enemy_type == "robot":
353 enemy = RobotEnemy()
354 elif enemy_type == "zombie":
355 enemy = ZombieEnemy()
356 enemy.center_x = math.floor(
357 cartesian[0] * TILE_SCALING * self.tile_map.tile_width
358 )
359 enemy.center_y = math.floor(
360 (cartesian[1] + 1) * (self.tile_map.tile_height * TILE_SCALING)
361 )
362 if "boundary_left" in my_object.properties:
363 enemy.boundary_left = my_object.properties["boundary_left"]
364 if "boundary_right" in my_object.properties:
365 enemy.boundary_right = my_object.properties["boundary_right"]
366 if "change_x" in my_object.properties:
367 enemy.change_x = my_object.properties["change_x"]
368 self.scene.add_sprite(LAYER_NAME_ENEMIES, enemy)
369
370 # Add bullet spritelist to Scene
371 self.scene.add_sprite_list(LAYER_NAME_BULLETS)
372
373 # --- Other stuff
374 # Set the background color
375 if self.tile_map.background_color:
376 arcade.set_background_color(self.tile_map.background_color)
377
378 # Create the 'physics engine'
379 self.physics_engine = arcade.PhysicsEnginePlatformer(
380 self.player_sprite,
381 platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS],
382 gravity_constant=GRAVITY,
383 ladders=self.scene[LAYER_NAME_LADDERS],
384 walls=self.scene[LAYER_NAME_PLATFORMS]
385 )
386
387 def on_show_view(self):
388 self.setup()
389
390 def on_draw(self):
391 """Render the screen."""
392
393 # Clear the screen to the background color
394 self.clear()
395
396 # Activate the game camera
397 self.camera.use()
398
399 # Draw our Scene
400 self.scene.draw()
401
402 # Draw hit boxes.
403 # self.scene[LAYER_NAME_COINS].draw_hit_boxes(color=arcade.color.WHITE)
404 # self.scene[LAYER_NAME_ENEMIES].draw_hit_boxes(color=arcade.color.WHITE)
405 # self.scene[LAYER_NAME_PLAYER].draw_hit_boxes(color=arcade.color.WHITE)
406
407 # Activate the GUI camera before drawing GUI elements
408 self.gui_camera.use()
409
410 # Draw our score on the screen, scrolling it with the viewport
411 score_text = f"Score: {self.score}"
412 arcade.draw_text(
413 score_text,
414 10,
415 10,
416 arcade.csscolor.BLACK,
417 18,
418 )
419
420 def process_keychange(self):
421 """
422 Called when we change a key up/down or we move on/off a ladder.
423 """
424 # Process up/down
425 if self.up_pressed and not self.down_pressed:
426 if self.physics_engine.is_on_ladder():
427 self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
428 elif (
429 self.physics_engine.can_jump(y_distance=10)
430 and not self.jump_needs_reset
431 ):
432 self.player_sprite.change_y = PLAYER_JUMP_SPEED
433 self.jump_needs_reset = True
434 arcade.play_sound(self.jump_sound)
435 elif self.down_pressed and not self.up_pressed:
436 if self.physics_engine.is_on_ladder():
437 self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
438
439 # Process up/down when on a ladder and no movement
440 if self.physics_engine.is_on_ladder():
441 if not self.up_pressed and not self.down_pressed:
442 self.player_sprite.change_y = 0
443 elif self.up_pressed and self.down_pressed:
444 self.player_sprite.change_y = 0
445
446 # Process left/right
447 if self.right_pressed and not self.left_pressed:
448 self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
449 elif self.left_pressed and not self.right_pressed:
450 self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
451 else:
452 self.player_sprite.change_x = 0
453
454 def on_key_press(self, key, modifiers):
455 """Called whenever a key is pressed."""
456
457 if key == arcade.key.UP or key == arcade.key.W:
458 self.up_pressed = True
459 elif key == arcade.key.DOWN or key == arcade.key.S:
460 self.down_pressed = True
461 elif key == arcade.key.LEFT or key == arcade.key.A:
462 self.left_pressed = True
463 elif key == arcade.key.RIGHT or key == arcade.key.D:
464 self.right_pressed = True
465
466 if key == arcade.key.Q:
467 self.shoot_pressed = True
468
469 if key == arcade.key.PLUS:
470 self.camera.zoom(0.01)
471 elif key == arcade.key.MINUS:
472 self.camera.zoom(-0.01)
473
474 self.process_keychange()
475
476 def on_key_release(self, key, modifiers):
477 """Called when the user releases a key."""
478
479 if key == arcade.key.UP or key == arcade.key.W:
480 self.up_pressed = False
481 self.jump_needs_reset = False
482 elif key == arcade.key.DOWN or key == arcade.key.S:
483 self.down_pressed = False
484 elif key == arcade.key.LEFT or key == arcade.key.A:
485 self.left_pressed = False
486 elif key == arcade.key.RIGHT or key == arcade.key.D:
487 self.right_pressed = False
488
489 if key == arcade.key.Q:
490 self.shoot_pressed = False
491
492 self.process_keychange()
493
494 def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
495 self.camera.zoom(-0.01 * scroll_y)
496
497 def center_camera_to_player(self, speed=0.2):
498 screen_center_x = (self.player_sprite.center_x - (self.camera.viewport_width / 2))
499 screen_center_y = (self.player_sprite.center_y - (self.camera.viewport_height / 2))
500 if screen_center_x < 0:
501 screen_center_x = 0
502 if screen_center_y < 0:
503 screen_center_y = 0
504 player_centered = (screen_center_x, screen_center_y)
505
506 self.camera.move_to(player_centered, speed)
507
508 def on_update(self, delta_time):
509 """Movement and game logic"""
510
511 # Move the player with the physics engine
512 self.physics_engine.update()
513
514 # Update animations
515 if self.physics_engine.can_jump():
516 self.player_sprite.can_jump = False
517 else:
518 self.player_sprite.can_jump = True
519
520 if self.physics_engine.is_on_ladder() and not self.physics_engine.can_jump():
521 self.player_sprite.is_on_ladder = True
522 self.process_keychange()
523 else:
524 self.player_sprite.is_on_ladder = False
525 self.process_keychange()
526
527 if self.can_shoot:
528 if self.shoot_pressed:
529 arcade.play_sound(self.shoot_sound)
530 bullet = arcade.Sprite(
531 ":resources:images/space_shooter/laserBlue01.png",
532 SPRITE_SCALING_LASER,
533 )
534
535 if self.player_sprite.facing_direction == RIGHT_FACING:
536 bullet.change_x = BULLET_SPEED
537 else:
538 bullet.change_x = -BULLET_SPEED
539
540 bullet.center_x = self.player_sprite.center_x
541 bullet.center_y = self.player_sprite.center_y
542
543 self.scene.add_sprite(LAYER_NAME_BULLETS, bullet)
544
545 self.can_shoot = False
546 else:
547 self.shoot_timer += 1
548 if self.shoot_timer == SHOOT_SPEED:
549 self.can_shoot = True
550 self.shoot_timer = 0
551
552 # Update Animations
553 self.scene.update_animation(
554 delta_time,
555 [
556 LAYER_NAME_COINS,
557 LAYER_NAME_BACKGROUND,
558 LAYER_NAME_PLAYER,
559 LAYER_NAME_ENEMIES,
560 ],
561 )
562
563 # Update moving platforms, enemies, and bullets
564 self.scene.update(
565 [LAYER_NAME_MOVING_PLATFORMS, LAYER_NAME_ENEMIES, LAYER_NAME_BULLETS]
566 )
567
568 # See if the enemy hit a boundary and needs to reverse direction.
569 for enemy in self.scene[LAYER_NAME_ENEMIES]:
570 if (
571 enemy.boundary_right
572 and enemy.right > enemy.boundary_right
573 and enemy.change_x > 0
574 ):
575 enemy.change_x *= -1
576
577 if (
578 enemy.boundary_left
579 and enemy.left < enemy.boundary_left
580 and enemy.change_x < 0
581 ):
582 enemy.change_x *= -1
583
584 for bullet in self.scene[LAYER_NAME_BULLETS]:
585 hit_list = arcade.check_for_collision_with_lists(
586 bullet,
587 [
588 self.scene[LAYER_NAME_ENEMIES],
589 self.scene[LAYER_NAME_PLATFORMS],
590 self.scene[LAYER_NAME_MOVING_PLATFORMS],
591 ],
592 )
593
594 if hit_list:
595 bullet.remove_from_sprite_lists()
596
597 for collision in hit_list:
598 if (
599 self.scene[LAYER_NAME_ENEMIES]
600 in collision.sprite_lists
601 ):
602 # The collision was with an enemy
603 collision.health -= BULLET_DAMAGE
604
605 if collision.health <= 0:
606 collision.remove_from_sprite_lists()
607 self.score += 100
608
609 # Hit sound
610 arcade.play_sound(self.hit_sound)
611
612 return
613
614 if (bullet.right < 0) or (
615 bullet.left
616 > (self.tile_map.width * self.tile_map.tile_width) * TILE_SCALING
617 ):
618 bullet.remove_from_sprite_lists()
619
620 player_collision_list = arcade.check_for_collision_with_lists(
621 self.player_sprite,
622 [
623 self.scene[LAYER_NAME_COINS],
624 self.scene[LAYER_NAME_ENEMIES],
625 ],
626 )
627
628 # Loop through each coin we hit (if any) and remove it
629 for collision in player_collision_list:
630
631 if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists:
632 arcade.play_sound(self.game_over)
633 game_over = GameOverView()
634 self.window.show_view(game_over)
635 return
636 else:
637 # Figure out how many points this coin is worth
638 if "Points" not in collision.properties:
639 print("Warning, collected a coin without a Points property.")
640 else:
641 points = int(collision.properties["Points"])
642 self.score += points
643
644 # Remove the coin
645 collision.remove_from_sprite_lists()
646 arcade.play_sound(self.collect_coin_sound)
647
648 # Position the camera
649 self.center_camera_to_player()
650
651
652class GameOverView(arcade.View):
653 """Class to manage the game overview"""
654
655 def on_show_view(self):
656 """Called when switching to this view"""
657 arcade.set_background_color(arcade.color.BLACK)
658
659 def on_draw(self):
660 """Draw the game overview"""
661 self.clear()
662 arcade.draw_text(
663 "Game Over - Click to restart",
664 SCREEN_WIDTH / 2,
665 SCREEN_HEIGHT / 2,
666 arcade.color.WHITE,
667 30,
668 anchor_x="center",
669 )
670
671 def on_mouse_press(self, _x, _y, _button, _modifiers):
672 """Use a mouse press to advance to the 'game' view."""
673 game_view = GameView()
674 self.window.show_view(game_view)
675
676
677def main():
678 """Main function"""
679 window = arcade.Window(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
680 menu_view = MainMenu()
681 window.show_view(menu_view)
682 arcade.run()
683
684
685if __name__ == "__main__":
686 main()