Step 14 - Moving Enemies#

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

Source Code#

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