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