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