Step 13 - Add Enemies#

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

Source Code#

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