Step 16 - Shooting Bullets#

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