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 texture = arcade.load_texture(filename)
62 return [texture, texture.flip_left_right()]
63
64
65class Entity(arcade.Sprite):
66 def __init__(self, name_folder, name_file):
67 super().__init__()
68
69 # Default to facing right
70 self.facing_direction = RIGHT_FACING
71
72 # Used for image sequences
73 self.cur_texture = 0
74 self.scale = CHARACTER_SCALING
75
76 main_path = f":resources:images/animated_characters/{name_folder}/{name_file}"
77
78 self.idle_texture_pair = load_texture_pair(f"{main_path}_idle.png")
79 self.jump_texture_pair = load_texture_pair(f"{main_path}_jump.png")
80 self.fall_texture_pair = load_texture_pair(f"{main_path}_fall.png")
81
82 # Load textures for walking
83 self.walk_textures = []
84 for i in range(8):
85 texture = load_texture_pair(f"{main_path}_walk{i}.png")
86 self.walk_textures.append(texture)
87
88 # Load textures for climbing
89 self.climbing_textures = []
90 texture = arcade.load_texture(f"{main_path}_climb0.png")
91 self.climbing_textures.append(texture)
92 texture = arcade.load_texture(f"{main_path}_climb1.png")
93 self.climbing_textures.append(texture)
94
95 # Set the initial texture
96 self.texture = self.idle_texture_pair[0]
97
98 # Hit box will be set based on the first image used. If you want to specify
99 # a different hit box, you can do it like the code below. Doing this when
100 # changing the texture for example would make the hitbox update whenever the
101 # texture is changed. This can be expensive so if the textures are very similar
102 # it may not be worth doing.
103 #
104 # self.hit_box = arcade.hitbox.RotatableHitBox(
105 # self.texture.hit_box_points,
106 # position=self.position,
107 # scale=self.scale_xy,
108 # angle=self.angle,
109 # )
110
111
112class Enemy(Entity):
113 def __init__(self, name_folder, name_file):
114 # Setup parent class
115 super().__init__(name_folder, name_file)
116
117 self.should_update_walk = 0
118 self.health = 0
119
120 def update_animation(self, delta_time: float = 1 / 60):
121 # Figure out if we need to flip face left or right
122 if self.change_x < 0 and self.facing_direction == RIGHT_FACING:
123 self.facing_direction = LEFT_FACING
124 elif self.change_x > 0 and self.facing_direction == LEFT_FACING:
125 self.facing_direction = RIGHT_FACING
126
127 # Idle animation
128 if self.change_x == 0:
129 self.texture = self.idle_texture_pair[self.facing_direction]
130 return
131
132 # Walking animation
133 if self.should_update_walk == 3:
134 self.cur_texture += 1
135 if self.cur_texture > 7:
136 self.cur_texture = 0
137 self.texture = self.walk_textures[self.cur_texture][self.facing_direction]
138 self.should_update_walk = 0
139 return
140
141 self.should_update_walk += 1
142
143
144class RobotEnemy(Enemy):
145 def __init__(self):
146 # Set up parent class
147 super().__init__("robot", "robot")
148
149 self.health = 100
150
151
152class ZombieEnemy(Enemy):
153 def __init__(self):
154 # Set up parent class
155 super().__init__("zombie", "zombie")
156
157 self.health = 50
158
159
160class PlayerCharacter(Entity):
161 """Player Sprite"""
162
163 def __init__(self):
164 # Set up parent class
165 super().__init__("male_person", "malePerson")
166
167 # Track our state
168 self.jumping = False
169 self.climbing = False
170 self.is_on_ladder = False
171
172 def update_animation(self, delta_time: float = 1 / 60):
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 self.window.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 self.window.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 self.scene.draw_hit_boxes(color=arcade.color.WHITE)
407
408 # Activate the GUI camera before drawing GUI elements
409 self.gui_camera.use()
410
411 # Draw our score on the screen, scrolling it with the viewport
412 score_text = f"Score: {self.score}"
413 arcade.draw_text(
414 score_text,
415 10,
416 10,
417 arcade.csscolor.BLACK,
418 18,
419 )
420
421 def process_keychange(self):
422 """
423 Called when we change a key up/down or we move on/off a ladder.
424 """
425 # Process up/down
426 if self.up_pressed and not self.down_pressed:
427 if self.physics_engine.is_on_ladder():
428 self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
429 elif (
430 self.physics_engine.can_jump(y_distance=10)
431 and not self.jump_needs_reset
432 ):
433 self.player_sprite.change_y = PLAYER_JUMP_SPEED
434 self.jump_needs_reset = True
435 arcade.play_sound(self.jump_sound)
436 elif self.down_pressed and not self.up_pressed:
437 if self.physics_engine.is_on_ladder():
438 self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
439
440 # Process up/down when on a ladder and no movement
441 if self.physics_engine.is_on_ladder():
442 if not self.up_pressed and not self.down_pressed:
443 self.player_sprite.change_y = 0
444 elif self.up_pressed and self.down_pressed:
445 self.player_sprite.change_y = 0
446
447 # Process left/right
448 if self.right_pressed and not self.left_pressed:
449 self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
450 elif self.left_pressed and not self.right_pressed:
451 self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
452 else:
453 self.player_sprite.change_x = 0
454
455 def on_key_press(self, key, modifiers):
456 """Called whenever a key is pressed."""
457
458 if key == arcade.key.UP or key == arcade.key.W:
459 self.up_pressed = True
460 elif key == arcade.key.DOWN or key == arcade.key.S:
461 self.down_pressed = True
462 elif key == arcade.key.LEFT or key == arcade.key.A:
463 self.left_pressed = True
464 elif key == arcade.key.RIGHT or key == arcade.key.D:
465 self.right_pressed = True
466
467 if key == arcade.key.Q:
468 self.shoot_pressed = True
469
470 if key == arcade.key.PLUS:
471 self.camera.zoom(0.01)
472 elif key == arcade.key.MINUS:
473 self.camera.zoom(-0.01)
474
475 self.process_keychange()
476
477 def on_key_release(self, key, modifiers):
478 """Called when the user releases a key."""
479
480 if key == arcade.key.UP or key == arcade.key.W:
481 self.up_pressed = False
482 self.jump_needs_reset = False
483 elif key == arcade.key.DOWN or key == arcade.key.S:
484 self.down_pressed = False
485 elif key == arcade.key.LEFT or key == arcade.key.A:
486 self.left_pressed = False
487 elif key == arcade.key.RIGHT or key == arcade.key.D:
488 self.right_pressed = False
489
490 if key == arcade.key.Q:
491 self.shoot_pressed = False
492
493 self.process_keychange()
494
495 def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
496 try:
497 self.camera.zoom += -0.01 * scroll_y
498 except Exception:
499 pass
500
501 def center_camera_to_player(self, speed=0.2):
502 screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2)
503 screen_center_y = self.player_sprite.center_y - (
504 self.camera.viewport_height / 2
505 )
506 if screen_center_x < 0:
507 screen_center_x = 0
508 if screen_center_y < 0:
509 screen_center_y = 0
510 player_centered = (screen_center_x, screen_center_y)
511
512 self.camera.move_to(player_centered, speed)
513
514 def on_update(self, delta_time):
515 """Movement and game logic"""
516
517 # Move the player with the physics engine
518 self.physics_engine.update()
519
520 # Update animations
521 if self.physics_engine.can_jump():
522 self.player_sprite.can_jump = False
523 else:
524 self.player_sprite.can_jump = True
525
526 if self.physics_engine.is_on_ladder() and not self.physics_engine.can_jump():
527 self.player_sprite.is_on_ladder = True
528 self.process_keychange()
529 else:
530 self.player_sprite.is_on_ladder = False
531 self.process_keychange()
532
533 if self.can_shoot:
534 if self.shoot_pressed:
535 arcade.play_sound(self.shoot_sound)
536 bullet = arcade.Sprite(
537 ":resources:images/space_shooter/laserBlue01.png",
538 SPRITE_SCALING_LASER,
539 )
540
541 if self.player_sprite.facing_direction == RIGHT_FACING:
542 bullet.change_x = BULLET_SPEED
543 else:
544 bullet.change_x = -BULLET_SPEED
545
546 bullet.center_x = self.player_sprite.center_x
547 bullet.center_y = self.player_sprite.center_y
548
549 self.scene.add_sprite(LAYER_NAME_BULLETS, bullet)
550
551 self.can_shoot = False
552 else:
553 self.shoot_timer += 1
554 if self.shoot_timer == SHOOT_SPEED:
555 self.can_shoot = True
556 self.shoot_timer = 0
557
558 # Update Animations
559 self.scene.update_animation(
560 delta_time,
561 [
562 LAYER_NAME_COINS,
563 LAYER_NAME_BACKGROUND,
564 LAYER_NAME_PLAYER,
565 LAYER_NAME_ENEMIES,
566 ],
567 )
568
569 # Update moving platforms, enemies, and bullets
570 self.scene.update(
571 [LAYER_NAME_MOVING_PLATFORMS, LAYER_NAME_ENEMIES, LAYER_NAME_BULLETS]
572 )
573
574 # See if the enemy hit a boundary and needs to reverse direction.
575 for enemy in self.scene[LAYER_NAME_ENEMIES]:
576 if (
577 enemy.boundary_right
578 and enemy.right > enemy.boundary_right
579 and enemy.change_x > 0
580 ):
581 enemy.change_x *= -1
582
583 if (
584 enemy.boundary_left
585 and enemy.left < enemy.boundary_left
586 and enemy.change_x < 0
587 ):
588 enemy.change_x *= -1
589
590 for bullet in self.scene[LAYER_NAME_BULLETS]:
591 hit_list = arcade.check_for_collision_with_lists(
592 bullet,
593 [
594 self.scene[LAYER_NAME_ENEMIES],
595 self.scene[LAYER_NAME_PLATFORMS],
596 self.scene[LAYER_NAME_MOVING_PLATFORMS],
597 ],
598 )
599
600 if hit_list:
601 bullet.remove_from_sprite_lists()
602
603 for collision in hit_list:
604 if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists:
605 # The collision was with an enemy
606 collision.health -= BULLET_DAMAGE
607
608 if collision.health <= 0:
609 collision.remove_from_sprite_lists()
610 self.score += 100
611
612 # Hit sound
613 arcade.play_sound(self.hit_sound)
614
615 return
616
617 if (bullet.right < 0) or (
618 bullet.left
619 > (self.tile_map.width * self.tile_map.tile_width) * TILE_SCALING
620 ):
621 bullet.remove_from_sprite_lists()
622
623 player_collision_list = arcade.check_for_collision_with_lists(
624 self.player_sprite,
625 [
626 self.scene[LAYER_NAME_COINS],
627 self.scene[LAYER_NAME_ENEMIES],
628 ],
629 )
630
631 # Loop through each coin we hit (if any) and remove it
632 for collision in player_collision_list:
633 if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists:
634 arcade.play_sound(self.game_over)
635 game_over = GameOverView()
636 self.window.show_view(game_over)
637 return
638 else:
639 # Figure out how many points this coin is worth
640 if "Points" not in collision.properties:
641 print("Warning, collected a coin without a Points property.")
642 else:
643 points = int(collision.properties["Points"])
644 self.score += points
645
646 # Remove the coin
647 collision.remove_from_sprite_lists()
648 arcade.play_sound(self.collect_coin_sound)
649
650 # Position the camera
651 self.center_camera_to_player()
652
653
654class GameOverView(arcade.View):
655 """Class to manage the game overview"""
656
657 def on_show_view(self):
658 """Called when switching to this view"""
659 self.window.background_color = arcade.color.BLACK
660
661 def on_draw(self):
662 """Draw the game overview"""
663 self.clear()
664 arcade.draw_text(
665 "Game Over - Click to restart",
666 SCREEN_WIDTH / 2,
667 SCREEN_HEIGHT / 2,
668 arcade.color.WHITE,
669 30,
670 anchor_x="center",
671 )
672
673 def on_mouse_press(self, _x, _y, _button, _modifiers):
674 """Use a mouse press to advance to the 'game' view."""
675 game_view = GameView()
676 self.window.show_view(game_view)
677
678
679def main():
680 """Main function"""
681 window = arcade.Window(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
682 menu_view = MainMenu()
683 window.show_view(menu_view)
684 arcade.run()
685
686
687if __name__ == "__main__":
688 main()
689if __name__ == "__main__":
690 main()
691 main()
692 main()