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

Source Code#

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