Step 11 - Add Character Animations, and Better Keyboard Control¶
Add character animations!
1"""
2Platformer Game
3
4python -m arcade.examples.platform_tutorial.11_animate_character
5"""
6import arcade
7import os
8
9# Constants
10SCREEN_WIDTH = 1000
11SCREEN_HEIGHT = 650
12SCREEN_TITLE = "Platformer"
13
14# Constants used to scale our sprites from their original size
15TILE_SCALING = 0.5
16CHARACTER_SCALING = TILE_SCALING * 2
17COIN_SCALING = TILE_SCALING
18SPRITE_PIXEL_SIZE = 128
19GRID_PIXEL_SIZE = (SPRITE_PIXEL_SIZE * TILE_SCALING)
20
21# Movement speed of player, in pixels per frame
22PLAYER_MOVEMENT_SPEED = 7
23GRAVITY = 1.5
24PLAYER_JUMP_SPEED = 30
25
26# How many pixels to keep as a minimum margin between the character
27# and the edge of the screen.
28LEFT_VIEWPORT_MARGIN = 200
29RIGHT_VIEWPORT_MARGIN = 200
30BOTTOM_VIEWPORT_MARGIN = 150
31TOP_VIEWPORT_MARGIN = 100
32
33PLAYER_START_X = SPRITE_PIXEL_SIZE * TILE_SCALING * 2
34PLAYER_START_Y = SPRITE_PIXEL_SIZE * TILE_SCALING * 1
35
36# Constants used to track if the player is facing left or right
37RIGHT_FACING = 0
38LEFT_FACING = 1
39
40
41def load_texture_pair(filename):
42 """
43 Load a texture pair, with the second being a mirror image.
44 """
45 return [
46 arcade.load_texture(filename),
47 arcade.load_texture(filename, flipped_horizontally=True)
48 ]
49
50
51class PlayerCharacter(arcade.Sprite):
52 """ Player Sprite"""
53 def __init__(self):
54
55 # Set up parent class
56 super().__init__()
57
58 # Default to face-right
59 self.character_face_direction = RIGHT_FACING
60
61 # Used for flipping between image sequences
62 self.cur_texture = 0
63 self.scale = CHARACTER_SCALING
64
65 # Track our state
66 self.jumping = False
67 self.climbing = False
68 self.is_on_ladder = False
69
70 # --- Load Textures ---
71
72 # Images from Kenney.nl's Asset Pack 3
73 # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
74 # main_path = ":resources:images/animated_characters/female_person/femalePerson"
75 main_path = ":resources:images/animated_characters/male_person/malePerson"
76 # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
77 # main_path = ":resources:images/animated_characters/zombie/zombie"
78 # main_path = ":resources:images/animated_characters/robot/robot"
79
80 # Load textures for idle standing
81 self.idle_texture_pair = load_texture_pair(f"{main_path}_idle.png")
82 self.jump_texture_pair = load_texture_pair(f"{main_path}_jump.png")
83 self.fall_texture_pair = load_texture_pair(f"{main_path}_fall.png")
84
85 # Load textures for walking
86 self.walk_textures = []
87 for i in range(8):
88 texture = load_texture_pair(f"{main_path}_walk{i}.png")
89 self.walk_textures.append(texture)
90
91 # Load textures for climbing
92 self.climbing_textures = []
93 texture = arcade.load_texture(f"{main_path}_climb0.png")
94 self.climbing_textures.append(texture)
95 texture = arcade.load_texture(f"{main_path}_climb1.png")
96 self.climbing_textures.append(texture)
97
98 # Set the initial texture
99 self.texture = self.idle_texture_pair[0]
100
101 # Hit box will be set based on the first image used. If you want to specify
102 # a different hit box, you can do it like the code below.
103 # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]])
104 self.set_hit_box(self.texture.hit_box_points)
105
106 def update_animation(self, delta_time: float = 1/60):
107
108 # Figure out if we need to flip face left or right
109 if self.change_x < 0 and self.character_face_direction == RIGHT_FACING:
110 self.character_face_direction = LEFT_FACING
111 elif self.change_x > 0 and self.character_face_direction == LEFT_FACING:
112 self.character_face_direction = RIGHT_FACING
113
114 # Climbing animation
115 if self.is_on_ladder:
116 self.climbing = True
117 if not self.is_on_ladder and self.climbing:
118 self.climbing = False
119 if self.climbing and abs(self.change_y) > 1:
120 self.cur_texture += 1
121 if self.cur_texture > 7:
122 self.cur_texture = 0
123 if self.climbing:
124 self.texture = self.climbing_textures[self.cur_texture // 4]
125 return
126
127 # Jumping animation
128 if self.change_y > 0 and not self.is_on_ladder:
129 self.texture = self.jump_texture_pair[self.character_face_direction]
130 return
131 elif self.change_y < 0 and not self.is_on_ladder:
132 self.texture = self.fall_texture_pair[self.character_face_direction]
133 return
134
135 # Idle animation
136 if self.change_x == 0:
137 self.texture = self.idle_texture_pair[self.character_face_direction]
138 return
139
140 # Walking animation
141 self.cur_texture += 1
142 if self.cur_texture > 7:
143 self.cur_texture = 0
144 self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]
145
146
147class MyGame(arcade.Window):
148 """
149 Main application class.
150 """
151
152 def __init__(self):
153 """
154 Initializer for the game
155 """
156
157 # Call the parent class and set up the window
158 super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
159
160 # Set the path to start with this program
161 file_path = os.path.dirname(os.path.abspath(__file__))
162 os.chdir(file_path)
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 # These are 'lists' that keep track of our sprites. Each sprite should
172 # go into a list.
173 self.coin_list = None
174 self.wall_list = None
175 self.background_list = None
176 self.ladder_list = None
177 self.player_list = None
178
179 # Separate variable that holds the player sprite
180 self.player_sprite = None
181
182 # Our 'physics' engine
183 self.physics_engine = None
184
185 # Used to keep track of our scrolling
186 self.view_bottom = 0
187 self.view_left = 0
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 # Used to keep track of our scrolling
203 self.view_bottom = 0
204 self.view_left = 0
205
206 # Keep track of the score
207 self.score = 0
208
209 # Create the Sprite lists
210 self.player_list = arcade.SpriteList()
211 self.background_list = arcade.SpriteList()
212 self.wall_list = arcade.SpriteList()
213 self.coin_list = arcade.SpriteList()
214
215 # Set up the player, specifically placing it at these coordinates.
216 self.player_sprite = PlayerCharacter()
217
218 self.player_sprite.center_x = PLAYER_START_X
219 self.player_sprite.center_y = PLAYER_START_Y
220 self.player_list.append(self.player_sprite)
221
222 # --- Load in a map from the tiled editor ---
223
224 # Name of the layer in the file that has our platforms/walls
225 platforms_layer_name = 'Platforms'
226 moving_platforms_layer_name = 'Moving Platforms'
227
228 # Name of the layer that has items for pick-up
229 coins_layer_name = 'Coins'
230
231 # Map name
232 map_name = f":resources:tmx_maps/map_with_ladders.tmx"
233
234 # Read in the tiled map
235 my_map = arcade.tilemap.read_tmx(map_name)
236
237 # Calculate the right edge of the my_map in pixels
238 self.end_of_map = my_map.map_size.width * GRID_PIXEL_SIZE
239
240 # -- Platforms
241 self.wall_list = arcade.tilemap.process_layer(my_map,
242 platforms_layer_name,
243 TILE_SCALING,
244 use_spatial_hash=True)
245
246 # -- Moving Platforms
247 moving_platforms_list = arcade.tilemap.process_layer(my_map, moving_platforms_layer_name, TILE_SCALING)
248 for sprite in moving_platforms_list:
249 self.wall_list.append(sprite)
250
251 # -- Background objects
252 self.background_list = arcade.tilemap.process_layer(my_map, "Background", TILE_SCALING)
253
254 # -- Background objects
255 self.ladder_list = arcade.tilemap.process_layer(my_map, "Ladders",
256 TILE_SCALING,
257 use_spatial_hash=True)
258
259 # -- Coins
260 self.coin_list = arcade.tilemap.process_layer(my_map, coins_layer_name,
261 TILE_SCALING,
262 use_spatial_hash=True)
263
264 # --- Other stuff
265 # Set the background color
266 if my_map.background_color:
267 arcade.set_background_color(my_map.background_color)
268
269 # Create the 'physics engine'
270 self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
271 self.wall_list,
272 gravity_constant=GRAVITY,
273 ladders=self.ladder_list)
274
275 def on_draw(self):
276 """ Render the screen. """
277
278 # Clear the screen to the background color
279 arcade.start_render()
280
281 # Draw our sprites
282 self.wall_list.draw()
283 self.background_list.draw()
284 self.ladder_list.draw()
285 self.coin_list.draw()
286 self.player_list.draw()
287
288 # Draw our score on the screen, scrolling it with the viewport
289 score_text = f"Score: {self.score}"
290 arcade.draw_text(score_text, 10 + self.view_left, 10 + self.view_bottom,
291 arcade.csscolor.BLACK, 18)
292
293 # Draw hit boxes.
294 # for wall in self.wall_list:
295 # wall.draw_hit_box(arcade.color.BLACK, 3)
296 #
297 # self.player_sprite.draw_hit_box(arcade.color.RED, 3)
298
299 def process_keychange(self):
300 """
301 Called when we change a key up/down or we move on/off a ladder.
302 """
303 # Process up/down
304 if self.up_pressed and not self.down_pressed:
305 if self.physics_engine.is_on_ladder():
306 self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
307 elif self.physics_engine.can_jump(y_distance=10) and not self.jump_needs_reset:
308 self.player_sprite.change_y = PLAYER_JUMP_SPEED
309 self.jump_needs_reset = True
310 arcade.play_sound(self.jump_sound)
311 elif self.down_pressed and not self.up_pressed:
312 if self.physics_engine.is_on_ladder():
313 self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
314
315 # Process up/down when on a ladder and no movement
316 if self.physics_engine.is_on_ladder():
317 if not self.up_pressed and not self.down_pressed:
318 self.player_sprite.change_y = 0
319 elif self.up_pressed and self.down_pressed:
320 self.player_sprite.change_y = 0
321
322 # Process left/right
323 if self.right_pressed and not self.left_pressed:
324 self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
325 elif self.left_pressed and not self.right_pressed:
326 self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
327 else:
328 self.player_sprite.change_x = 0
329
330 def on_key_press(self, key, modifiers):
331 """Called whenever a key is pressed. """
332
333 if key == arcade.key.UP or key == arcade.key.W:
334 self.up_pressed = True
335 elif key == arcade.key.DOWN or key == arcade.key.S:
336 self.down_pressed = True
337 elif key == arcade.key.LEFT or key == arcade.key.A:
338 self.left_pressed = True
339 elif key == arcade.key.RIGHT or key == arcade.key.D:
340 self.right_pressed = True
341
342 self.process_keychange()
343
344 def on_key_release(self, key, modifiers):
345 """Called when the user releases a key. """
346
347 if key == arcade.key.UP or key == arcade.key.W:
348 self.up_pressed = False
349 self.jump_needs_reset = False
350 elif key == arcade.key.DOWN or key == arcade.key.S:
351 self.down_pressed = False
352 elif key == arcade.key.LEFT or key == arcade.key.A:
353 self.left_pressed = False
354 elif key == arcade.key.RIGHT or key == arcade.key.D:
355 self.right_pressed = False
356
357 self.process_keychange()
358
359 def on_update(self, delta_time):
360 """ Movement and game logic """
361
362 # Move the player with the physics engine
363 self.physics_engine.update()
364
365 # Update animations
366 if self.physics_engine.can_jump():
367 self.player_sprite.can_jump = False
368 else:
369 self.player_sprite.can_jump = True
370
371 if self.physics_engine.is_on_ladder() and not self.physics_engine.can_jump():
372 self.player_sprite.is_on_ladder = True
373 self.process_keychange()
374 else:
375 self.player_sprite.is_on_ladder = False
376 self.process_keychange()
377
378 self.coin_list.update_animation(delta_time)
379 self.background_list.update_animation(delta_time)
380 self.player_list.update_animation(delta_time)
381
382 # Update walls, used with moving platforms
383 self.wall_list.update()
384
385 # See if the moving wall hit a boundary and needs to reverse direction.
386 for wall in self.wall_list:
387
388 if wall.boundary_right and wall.right > wall.boundary_right and wall.change_x > 0:
389 wall.change_x *= -1
390 if wall.boundary_left and wall.left < wall.boundary_left and wall.change_x < 0:
391 wall.change_x *= -1
392 if wall.boundary_top and wall.top > wall.boundary_top and wall.change_y > 0:
393 wall.change_y *= -1
394 if wall.boundary_bottom and wall.bottom < wall.boundary_bottom and wall.change_y < 0:
395 wall.change_y *= -1
396
397 # See if we hit any coins
398 coin_hit_list = arcade.check_for_collision_with_list(self.player_sprite,
399 self.coin_list)
400
401 # Loop through each coin we hit (if any) and remove it
402 for coin in coin_hit_list:
403
404 # Figure out how many points this coin is worth
405 if 'Points' not in coin.properties:
406 print("Warning, collected a coin without a Points property.")
407 else:
408 points = int(coin.properties['Points'])
409 self.score += points
410
411 # Remove the coin
412 coin.remove_from_sprite_lists()
413 arcade.play_sound(self.collect_coin_sound)
414
415 # Track if we need to change the viewport
416 changed_viewport = False
417
418 # --- Manage Scrolling ---
419
420 # Scroll left
421 left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
422 if self.player_sprite.left < left_boundary:
423 self.view_left -= left_boundary - self.player_sprite.left
424 changed_viewport = True
425
426 # Scroll right
427 right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
428 if self.player_sprite.right > right_boundary:
429 self.view_left += self.player_sprite.right - right_boundary
430 changed_viewport = True
431
432 # Scroll up
433 top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
434 if self.player_sprite.top > top_boundary:
435 self.view_bottom += self.player_sprite.top - top_boundary
436 changed_viewport = True
437
438 # Scroll down
439 bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
440 if self.player_sprite.bottom < bottom_boundary:
441 self.view_bottom -= bottom_boundary - self.player_sprite.bottom
442 changed_viewport = True
443
444 if changed_viewport:
445 # Only scroll to integers. Otherwise we end up with pixels that
446 # don't line up on the screen
447 self.view_bottom = int(self.view_bottom)
448 self.view_left = int(self.view_left)
449
450 # Do the scrolling
451 arcade.set_viewport(self.view_left,
452 SCREEN_WIDTH + self.view_left,
453 self.view_bottom,
454 SCREEN_HEIGHT + self.view_bottom)
455
456
457def main():
458 """ Main method """
459 window = MyGame()
460 window.setup()
461 arcade.run()
462
463
464if __name__ == "__main__":
465 main()