Step 15 - Collision with Enemies#

Collision with Enemies#
  1"""
  2Platformer Game
  3
  4python -m arcade.examples.platform_tutorial.15_collision_with_enemies
  5"""
  6from __future__ import annotations
  7
  8import math
  9
 10import arcade
 11
 12# Constants
 13SCREEN_WIDTH = 1000
 14SCREEN_HEIGHT = 650
 15SCREEN_TITLE = "Platformer"
 16
 17# Constants used to scale our sprites from their original size
 18TILE_SCALING = 0.5
 19CHARACTER_SCALING = TILE_SCALING * 2
 20COIN_SCALING = TILE_SCALING
 21SPRITE_PIXEL_SIZE = 128
 22GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING
 23
 24# Movement speed of player, in pixels per frame
 25PLAYER_MOVEMENT_SPEED = 7
 26GRAVITY = 1.5
 27PLAYER_JUMP_SPEED = 30
 28
 29# How many pixels to keep as a minimum margin between the character
 30# and the edge of the screen.
 31LEFT_VIEWPORT_MARGIN = 200
 32RIGHT_VIEWPORT_MARGIN = 200
 33BOTTOM_VIEWPORT_MARGIN = 150
 34TOP_VIEWPORT_MARGIN = 100
 35
 36PLAYER_START_X = 2
 37PLAYER_START_Y = 1
 38
 39# Constants used to track if the player is facing left or right
 40RIGHT_FACING = 0
 41LEFT_FACING = 1
 42
 43LAYER_NAME_MOVING_PLATFORMS = "Moving Platforms"
 44LAYER_NAME_PLATFORMS = "Platforms"
 45LAYER_NAME_COINS = "Coins"
 46LAYER_NAME_BACKGROUND = "Background"
 47LAYER_NAME_LADDERS = "Ladders"
 48LAYER_NAME_PLAYER = "Player"
 49LAYER_NAME_ENEMIES = "Enemies"
 50
 51
 52def load_texture_pair(filename):
 53    """
 54    Load a texture pair, with the second being a mirror image.
 55    """
 56    return [
 57        arcade.load_texture(filename),
 58        arcade.load_texture(filename, flipped_horizontally=True),
 59    ]
 60
 61
 62class Entity(arcade.Sprite):
 63    def __init__(self, name_folder, name_file):
 64        super().__init__()
 65
 66        # Default to facing right
 67        self.facing_direction = RIGHT_FACING
 68
 69        # Used for image sequences
 70        self.cur_texture = 0
 71        self.scale = CHARACTER_SCALING
 72
 73        main_path = f":resources:images/animated_characters/{name_folder}/{name_file}"
 74
 75        self.idle_texture_pair = load_texture_pair(f"{main_path}_idle.png")
 76        self.jump_texture_pair = load_texture_pair(f"{main_path}_jump.png")
 77        self.fall_texture_pair = load_texture_pair(f"{main_path}_fall.png")
 78
 79        # Load textures for walking
 80        self.walk_textures = []
 81        for i in range(8):
 82            texture = load_texture_pair(f"{main_path}_walk{i}.png")
 83            self.walk_textures.append(texture)
 84
 85        # Load textures for climbing
 86        self.climbing_textures = []
 87        texture = arcade.load_texture(f"{main_path}_climb0.png")
 88        self.climbing_textures.append(texture)
 89        texture = arcade.load_texture(f"{main_path}_climb1.png")
 90        self.climbing_textures.append(texture)
 91
 92        # Set the initial texture
 93        self.texture = self.idle_texture_pair[0]
 94
 95        # Hit box will be set based on the first image used. If you want to specify
 96        # a different hit box, you can do it like the code below. Doing this when
 97        # changing the texture for example would make the hitbox update whenever the
 98        # texture is changed. This can be expensive so if the textures are very similar
 99        # it may not be worth doing.
100        #
101        # self.hit_box = arcade.hitbox.RotatableHitBox(
102        #     self.texture.hit_box_points,
103        #     position=self.position,
104        #     scale=self.scale_xy,
105        #     angle=self.angle,
106        # )
107
108
109class Enemy(Entity):
110    def __init__(self, name_folder, name_file):
111        # Setup parent class
112        super().__init__(name_folder, name_file)
113
114        self.should_update_walk = 0
115
116    def update_animation(self, delta_time: float = 1 / 60):
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        # Set up parent class
143        super().__init__("robot", "robot")
144
145
146class ZombieEnemy(Enemy):
147    def __init__(self):
148        # Set up parent class
149        super().__init__("zombie", "zombie")
150
151
152class PlayerCharacter(Entity):
153    """Player Sprite"""
154
155    def __init__(self):
156        # Set up parent class
157        super().__init__("male_person", "malePerson")
158
159        # Track our state
160        self.jumping = False
161        self.climbing = False
162        self.is_on_ladder = False
163
164    def update_animation(self, delta_time: float = 1 / 60):
165        # Figure out if we need to flip face left or right
166        if self.change_x < 0 and self.facing_direction == RIGHT_FACING:
167            self.facing_direction = LEFT_FACING
168        elif self.change_x > 0 and self.facing_direction == LEFT_FACING:
169            self.facing_direction = RIGHT_FACING
170
171        # Climbing animation
172        if self.is_on_ladder:
173            self.climbing = True
174        if not self.is_on_ladder and self.climbing:
175            self.climbing = False
176        if self.climbing and abs(self.change_y) > 1:
177            self.cur_texture += 1
178            if self.cur_texture > 7:
179                self.cur_texture = 0
180        if self.climbing:
181            self.texture = self.climbing_textures[self.cur_texture // 4]
182            return
183
184        # Jumping animation
185        if self.change_y > 0 and not self.is_on_ladder:
186            self.texture = self.jump_texture_pair[self.facing_direction]
187            return
188        elif self.change_y < 0 and not self.is_on_ladder:
189            self.texture = self.fall_texture_pair[self.facing_direction]
190            return
191
192        # Idle animation
193        if self.change_x == 0:
194            self.texture = self.idle_texture_pair[self.facing_direction]
195            return
196
197        # Walking animation
198        self.cur_texture += 1
199        if self.cur_texture > 7:
200            self.cur_texture = 0
201        self.texture = self.walk_textures[self.cur_texture][self.facing_direction]
202
203
204class MyGame(arcade.Window):
205    """
206    Main application class.
207    """
208
209    def __init__(self):
210        """
211        Initializer for the game
212        """
213        # Call the parent class and set up the window
214        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
215
216        # Track the current state of what key is pressed
217        self.left_pressed = False
218        self.right_pressed = False
219        self.up_pressed = False
220        self.down_pressed = False
221        self.jump_needs_reset = False
222
223        # Our TileMap Object
224        self.tile_map = None
225
226        # Our Scene Object
227        self.scene = None
228
229        # Separate variable that holds the player sprite
230        self.player_sprite = None
231
232        # Our 'physics' engine
233        self.physics_engine = None
234
235        # A Camera that can be used for scrolling the screen
236        self.camera = None
237
238        # A Camera that can be used to draw GUI elements
239        self.gui_camera = None
240
241        self.end_of_map = 0
242
243        # Keep track of the score
244        self.score = 0
245
246        # Load sounds
247        self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
248        self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
249        self.game_over = arcade.load_sound(":resources:sounds/gameover1.wav")
250
251    def setup(self):
252        """Set up the game here. Call this function to restart the game."""
253
254        # Set up the Cameras
255        viewport = (0, 0, self.width, self.height)
256        self.camera = arcade.SimpleCamera(viewport=viewport)
257        self.gui_camera = arcade.SimpleCamera(viewport=viewport)
258
259        # Map name
260        map_name = ":resources:tiled_maps/map_with_ladders.json"
261
262        # Layer Specific Options for the Tilemap
263        layer_options = {
264            LAYER_NAME_PLATFORMS: {
265                "use_spatial_hash": True,
266            },
267            LAYER_NAME_MOVING_PLATFORMS: {
268                "use_spatial_hash": False,
269            },
270            LAYER_NAME_LADDERS: {
271                "use_spatial_hash": True,
272            },
273            LAYER_NAME_COINS: {
274                "use_spatial_hash": True,
275            },
276        }
277
278        # Load in TileMap
279        self.tile_map = arcade.load_tilemap(map_name, TILE_SCALING, layer_options)
280
281        # Initiate New Scene with our TileMap, this will automatically add all layers
282        # from the map as SpriteLists in the scene in the proper order.
283        self.scene = arcade.Scene.from_tilemap(self.tile_map)
284
285        # Keep track of the score
286        self.score = 0
287
288        # Set up the player, specifically placing it at these coordinates.
289        self.player_sprite = PlayerCharacter()
290        self.player_sprite.center_x = (
291            self.tile_map.tile_width * TILE_SCALING * PLAYER_START_X
292        )
293        self.player_sprite.center_y = (
294            self.tile_map.tile_height * TILE_SCALING * PLAYER_START_Y
295        )
296        self.scene.add_sprite(LAYER_NAME_PLAYER, self.player_sprite)
297
298        # Calculate the right edge of the my_map in pixels
299        self.end_of_map = self.tile_map.width * GRID_PIXEL_SIZE
300
301        # -- Enemies
302        enemies_layer = self.tile_map.object_lists[LAYER_NAME_ENEMIES]
303
304        for my_object in enemies_layer:
305            cartesian = self.tile_map.get_cartesian(
306                my_object.shape[0], my_object.shape[1]
307            )
308            enemy_type = my_object.properties["type"]
309            if enemy_type == "robot":
310                enemy = RobotEnemy()
311            elif enemy_type == "zombie":
312                enemy = ZombieEnemy()
313            enemy.center_x = math.floor(
314                cartesian[0] * TILE_SCALING * self.tile_map.tile_width
315            )
316            enemy.center_y = math.floor(
317                (cartesian[1] + 1) * (self.tile_map.tile_height * TILE_SCALING)
318            )
319            if "boundary_left" in my_object.properties:
320                enemy.boundary_left = my_object.properties["boundary_left"]
321            if "boundary_right" in my_object.properties:
322                enemy.boundary_right = my_object.properties["boundary_right"]
323            if "change_x" in my_object.properties:
324                enemy.change_x = my_object.properties["change_x"]
325            self.scene.add_sprite(LAYER_NAME_ENEMIES, enemy)
326
327        # --- Other stuff
328        # Set the background color
329        if self.tile_map.background_color:
330            self.background_color = self.tile_map.background_color
331
332        # Create the 'physics engine'
333        self.physics_engine = arcade.PhysicsEnginePlatformer(
334            self.player_sprite,
335            platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS],
336            gravity_constant=GRAVITY,
337            ladders=self.scene[LAYER_NAME_LADDERS],
338            walls=self.scene[LAYER_NAME_PLATFORMS],
339        )
340
341    def on_draw(self):
342        """Render the screen."""
343
344        # Clear the screen to the background color
345        self.clear()
346
347        # Activate the game camera
348        self.camera.use()
349
350        # Draw our Scene
351        self.scene.draw()
352
353        # Activate the GUI camera before drawing GUI elements
354        self.gui_camera.use()
355
356        # Draw our score on the screen, scrolling it with the viewport
357        score_text = f"Score: {self.score}"
358        arcade.draw_text(
359            score_text,
360            10,
361            10,
362            arcade.csscolor.BLACK,
363            18,
364        )
365
366        # Draw hit boxes.
367        # for wall in self.wall_list:
368        #     wall.draw_hit_box(arcade.color.BLACK, 3)
369        #
370        # self.player_sprite.draw_hit_box(arcade.color.RED, 3)
371
372    def process_keychange(self):
373        """
374        Called when we change a key up/down, or we move on/off a ladder.
375        """
376        # Process up/down
377        if self.up_pressed and not self.down_pressed:
378            if self.physics_engine.is_on_ladder():
379                self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
380            elif (
381                self.physics_engine.can_jump(y_distance=10)
382                and not self.jump_needs_reset
383            ):
384                self.player_sprite.change_y = PLAYER_JUMP_SPEED
385                self.jump_needs_reset = True
386                arcade.play_sound(self.jump_sound)
387        elif self.down_pressed and not self.up_pressed:
388            if self.physics_engine.is_on_ladder():
389                self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
390
391        # Process up/down when on a ladder and no movement
392        if self.physics_engine.is_on_ladder():
393            if not self.up_pressed and not self.down_pressed:
394                self.player_sprite.change_y = 0
395            elif self.up_pressed and self.down_pressed:
396                self.player_sprite.change_y = 0
397
398        # Process left/right
399        if self.right_pressed and not self.left_pressed:
400            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
401        elif self.left_pressed and not self.right_pressed:
402            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
403        else:
404            self.player_sprite.change_x = 0
405
406    def on_key_press(self, key, modifiers):
407        """Called whenever a key is pressed."""
408
409        if key == arcade.key.UP or key == arcade.key.W:
410            self.up_pressed = True
411        elif key == arcade.key.DOWN or key == arcade.key.S:
412            self.down_pressed = True
413        elif key == arcade.key.LEFT or key == arcade.key.A:
414            self.left_pressed = True
415        elif key == arcade.key.RIGHT or key == arcade.key.D:
416            self.right_pressed = True
417
418        self.process_keychange()
419
420    def on_key_release(self, key, modifiers):
421        """Called when the user releases a key."""
422
423        if key == arcade.key.UP or key == arcade.key.W:
424            self.up_pressed = False
425            self.jump_needs_reset = False
426        elif key == arcade.key.DOWN or key == arcade.key.S:
427            self.down_pressed = False
428        elif key == arcade.key.LEFT or key == arcade.key.A:
429            self.left_pressed = False
430        elif key == arcade.key.RIGHT or key == arcade.key.D:
431            self.right_pressed = False
432
433        self.process_keychange()
434
435    def center_camera_to_player(self, speed=0.2):
436        screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2)
437        screen_center_y = self.player_sprite.center_y - (
438            self.camera.viewport_height / 2
439        )
440        if screen_center_x < 0:
441            screen_center_x = 0
442        if screen_center_y < 0:
443            screen_center_y = 0
444        player_centered = screen_center_x, screen_center_y
445
446        self.camera.move_to(player_centered, speed)
447
448    def on_update(self, delta_time):
449        """Movement and game logic"""
450
451        # Move the player with the physics engine
452        self.physics_engine.update()
453
454        # Update animations
455        if self.physics_engine.can_jump():
456            self.player_sprite.can_jump = False
457        else:
458            self.player_sprite.can_jump = True
459
460        if self.physics_engine.is_on_ladder() and not self.physics_engine.can_jump():
461            self.player_sprite.is_on_ladder = True
462            self.process_keychange()
463        else:
464            self.player_sprite.is_on_ladder = False
465            self.process_keychange()
466
467        # Update Animations
468        self.scene.update_animation(
469            delta_time,
470            [
471                LAYER_NAME_COINS,
472                LAYER_NAME_BACKGROUND,
473                LAYER_NAME_PLAYER,
474                LAYER_NAME_ENEMIES,
475            ],
476        )
477
478        # Update moving platforms and enemies
479        self.scene.update([LAYER_NAME_MOVING_PLATFORMS, LAYER_NAME_ENEMIES])
480
481        # See if the enemy hit a boundary and needs to reverse direction.
482        for enemy in self.scene[LAYER_NAME_ENEMIES]:
483            if (
484                enemy.boundary_right
485                and enemy.right > enemy.boundary_right
486                and enemy.change_x > 0
487            ):
488                enemy.change_x *= -1
489
490            if (
491                enemy.boundary_left
492                and enemy.left < enemy.boundary_left
493                and enemy.change_x < 0
494            ):
495                enemy.change_x *= -1
496
497        player_collision_list = arcade.check_for_collision_with_lists(
498            self.player_sprite,
499            [
500                self.scene[LAYER_NAME_COINS],
501                self.scene[LAYER_NAME_ENEMIES],
502            ],
503        )
504
505        # # See if we hit any coins
506        # coin_hit_list = arcade.check_for_collision_with_list(
507        #     self.player_sprite, self.scene.get_sprite_list(LAYER_NAME_COINS)
508        # )
509
510        # Loop through each coin we hit (if any) and remove it
511        for collision in player_collision_list:
512            if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists:
513                arcade.play_sound(self.game_over)
514                self.setup()
515                return
516            else:
517                # Figure out how many points this coin is worth
518                if "Points" not in collision.properties:
519                    print("Warning, collected a coin without a Points property.")
520                else:
521                    points = int(collision.properties["Points"])
522                    self.score += points
523
524                # Remove the coin
525                collision.remove_from_sprite_lists()
526                arcade.play_sound(self.collect_coin_sound)
527
528        # Position the camera
529        self.center_camera_to_player()
530
531
532def main():
533    """Main function"""
534    window = MyGame()
535    window.setup()
536    arcade.run()
537
538
539if __name__ == "__main__":
540    main()