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