Step 13 - Add Enemies#

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

Source Code#

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