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