Step 12 - Add Character Animations, and Better Keyboard Control#

Add character animations!

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

Source Code#

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