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()