Step 16 - Shooting Bullets#
Shooting Bullets#
1"""
2Platformer Game
3
4python -m arcade.examples.platform_tutorial.16_shooting_bullets
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# Shooting Constants
23SPRITE_SCALING_LASER = 0.8
24SHOOT_SPEED = 15
25BULLET_SPEED = 12
26BULLET_DAMAGE = 25
27
28# Movement speed of player, in pixels per frame
29PLAYER_MOVEMENT_SPEED = 7
30GRAVITY = 1.5
31PLAYER_JUMP_SPEED = 30
32
33# How many pixels to keep as a minimum margin between the character
34# and the edge of the screen.
35LEFT_VIEWPORT_MARGIN = 200
36RIGHT_VIEWPORT_MARGIN = 200
37BOTTOM_VIEWPORT_MARGIN = 150
38TOP_VIEWPORT_MARGIN = 100
39
40PLAYER_START_X = 2
41PLAYER_START_Y = 1
42
43# Constants used to track if the player is facing left or right
44RIGHT_FACING = 0
45LEFT_FACING = 1
46
47LAYER_NAME_MOVING_PLATFORMS = "Moving Platforms"
48LAYER_NAME_PLATFORMS = "Platforms"
49LAYER_NAME_COINS = "Coins"
50LAYER_NAME_BACKGROUND = "Background"
51LAYER_NAME_LADDERS = "Ladders"
52LAYER_NAME_PLAYER = "Player"
53LAYER_NAME_ENEMIES = "Enemies"
54LAYER_NAME_BULLETS = "Bullets"
55
56
57def load_texture_pair(filename):
58 """
59 Load a texture pair, with the second being a mirror image.
60 """
61 return [
62 arcade.load_texture(filename),
63 arcade.load_texture(filename, flipped_horizontally=True),
64 ]
65
66
67class Entity(arcade.Sprite):
68 def __init__(self, name_folder, name_file):
69 super().__init__()
70
71 # Default to facing right
72 self.facing_direction = RIGHT_FACING
73
74 # Used for image sequences
75 self.cur_texture = 0
76 self.scale = CHARACTER_SCALING
77
78 main_path = f":resources:images/animated_characters/{name_folder}/{name_file}"
79
80 self.idle_texture_pair = load_texture_pair(f"{main_path}_idle.png")
81 self.jump_texture_pair = load_texture_pair(f"{main_path}_jump.png")
82 self.fall_texture_pair = load_texture_pair(f"{main_path}_fall.png")
83
84 # Load textures for walking
85 self.walk_textures = []
86 for i in range(8):
87 texture = load_texture_pair(f"{main_path}_walk{i}.png")
88 self.walk_textures.append(texture)
89
90 # Load textures for climbing
91 self.climbing_textures = []
92 texture = arcade.load_texture(f"{main_path}_climb0.png")
93 self.climbing_textures.append(texture)
94 texture = arcade.load_texture(f"{main_path}_climb1.png")
95 self.climbing_textures.append(texture)
96
97 # Set the initial texture
98 self.texture = self.idle_texture_pair[0]
99
100 # Hit box will be set based on the first image used. If you want to specify
101 # a different hit box, you can do it like the code below. Doing this when
102 # changing the texture for example would make the hitbox update whenever the
103 # texture is changed. This can be expensive so if the textures are very similar
104 # it may not be worth doing.
105 #
106 # self.hit_box = arcade.hitbox.RotatableHitBox(
107 # self.texture.hit_box_points,
108 # position=self.position,
109 # scale=self.scale_xy,
110 # angle=self.angle,
111 # )
112
113
114class Enemy(Entity):
115 def __init__(self, name_folder, name_file):
116 # Setup parent class
117 super().__init__(name_folder, name_file)
118
119 self.should_update_walk = 0
120 self.health = 0
121
122 def update_animation(self, delta_time: float = 1 / 60):
123 # Figure out if we need to flip face left or right
124 if self.change_x < 0 and self.facing_direction == RIGHT_FACING:
125 self.facing_direction = LEFT_FACING
126 elif self.change_x > 0 and self.facing_direction == LEFT_FACING:
127 self.facing_direction = RIGHT_FACING
128
129 # Idle animation
130 if self.change_x == 0:
131 self.texture = self.idle_texture_pair[self.facing_direction]
132 return
133
134 # Walking animation
135 if self.should_update_walk == 3:
136 self.cur_texture += 1
137 if self.cur_texture > 7:
138 self.cur_texture = 0
139 self.texture = self.walk_textures[self.cur_texture][self.facing_direction]
140 self.should_update_walk = 0
141 return
142
143 self.should_update_walk += 1
144
145
146class RobotEnemy(Enemy):
147 def __init__(self):
148 # Set up parent class
149 super().__init__("robot", "robot")
150
151 self.health = 100
152
153
154class ZombieEnemy(Enemy):
155 def __init__(self):
156 # Set up parent class
157 super().__init__("zombie", "zombie")
158
159 self.health = 50
160
161
162class PlayerCharacter(Entity):
163 """Player Sprite"""
164
165 def __init__(self):
166 # Set up parent class
167 super().__init__("male_person", "malePerson")
168
169 # Track our state
170 self.jumping = False
171 self.climbing = False
172 self.is_on_ladder = False
173
174 def update_animation(self, delta_time: float = 1 / 60):
175 # Figure out if we need to flip face left or right
176 if self.change_x < 0 and self.facing_direction == RIGHT_FACING:
177 self.facing_direction = LEFT_FACING
178 elif self.change_x > 0 and self.facing_direction == LEFT_FACING:
179 self.facing_direction = RIGHT_FACING
180
181 # Climbing animation
182 if self.is_on_ladder:
183 self.climbing = True
184 if not self.is_on_ladder and self.climbing:
185 self.climbing = False
186 if self.climbing and abs(self.change_y) > 1:
187 self.cur_texture += 1
188 if self.cur_texture > 7:
189 self.cur_texture = 0
190 if self.climbing:
191 self.texture = self.climbing_textures[self.cur_texture // 4]
192 return
193
194 # Jumping animation
195 if self.change_y > 0 and not self.is_on_ladder:
196 self.texture = self.jump_texture_pair[self.facing_direction]
197 return
198 elif self.change_y < 0 and not self.is_on_ladder:
199 self.texture = self.fall_texture_pair[self.facing_direction]
200 return
201
202 # Idle animation
203 if self.change_x == 0:
204 self.texture = self.idle_texture_pair[self.facing_direction]
205 return
206
207 # Walking animation
208 self.cur_texture += 1
209 if self.cur_texture > 7:
210 self.cur_texture = 0
211 self.texture = self.walk_textures[self.cur_texture][self.facing_direction]
212
213
214class MyGame(arcade.Window):
215 """
216 Main application class.
217 """
218
219 def __init__(self):
220 """
221 Initializer for the game
222 """
223 # Call the parent class and set up the window
224 super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
225
226 # Track the current state of what key is pressed
227 self.left_pressed = False
228 self.right_pressed = False
229 self.up_pressed = False
230 self.down_pressed = False
231 self.shoot_pressed = False
232 self.jump_needs_reset = False
233
234 # Our TileMap Object
235 self.tile_map = None
236
237 # Our Scene Object
238 self.scene = None
239
240 # Separate variable that holds the player sprite
241 self.player_sprite = None
242
243 # Our 'physics' engine
244 self.physics_engine = None
245
246 # A Camera that can be used for scrolling the screen
247 self.camera = None
248
249 # A Camera that can be used to draw GUI elements
250 self.gui_camera = None
251
252 self.end_of_map = 0
253
254 # Keep track of the score
255 self.score = 0
256
257 # Shooting mechanics
258 self.can_shoot = False
259 self.shoot_timer = 0
260
261 # Load sounds
262 self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
263 self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
264 self.game_over = arcade.load_sound(":resources:sounds/gameover1.wav")
265 self.shoot_sound = arcade.load_sound(":resources:sounds/hurt5.wav")
266 self.hit_sound = arcade.load_sound(":resources:sounds/hit5.wav")
267
268 def setup(self):
269 """Set up the game here. Call this function to restart the game."""
270
271 # Setup the Cameras
272 viewport = (0, 0, self.width, self.height)
273 self.camera = arcade.SimpleCamera(viewport=viewport)
274 self.gui_camera = arcade.SimpleCamera(viewport=viewport)
275
276 # Map name
277 map_name = ":resources:tiled_maps/map_with_ladders.json"
278
279 # Layer Specific Options for the Tilemap
280 layer_options = {
281 LAYER_NAME_PLATFORMS: {
282 "use_spatial_hash": True,
283 },
284 LAYER_NAME_MOVING_PLATFORMS: {
285 "use_spatial_hash": False,
286 },
287 LAYER_NAME_LADDERS: {
288 "use_spatial_hash": True,
289 },
290 LAYER_NAME_COINS: {
291 "use_spatial_hash": True,
292 },
293 }
294
295 # Load in TileMap
296 self.tile_map = arcade.load_tilemap(map_name, TILE_SCALING, layer_options)
297
298 # Initiate New Scene with our TileMap, this will automatically add all layers
299 # from the map as SpriteLists in the scene in the proper order.
300 self.scene = arcade.Scene.from_tilemap(self.tile_map)
301
302 # Keep track of the score
303 self.score = 0
304
305 # Shooting mechanics
306 self.can_shoot = True
307 self.shoot_timer = 0
308
309 # Set up the player, specifically placing it at these coordinates.
310 self.player_sprite = PlayerCharacter()
311 self.player_sprite.center_x = (
312 self.tile_map.tile_width * TILE_SCALING * PLAYER_START_X
313 )
314 self.player_sprite.center_y = (
315 self.tile_map.tile_height * TILE_SCALING * PLAYER_START_Y
316 )
317 self.scene.add_sprite(LAYER_NAME_PLAYER, self.player_sprite)
318
319 # Calculate the right edge of the my_map in pixels
320 self.end_of_map = self.tile_map.width * GRID_PIXEL_SIZE
321
322 # -- Enemies
323 enemies_layer = self.tile_map.object_lists[LAYER_NAME_ENEMIES]
324
325 for my_object in enemies_layer:
326 cartesian = self.tile_map.get_cartesian(
327 my_object.shape[0], my_object.shape[1]
328 )
329 enemy_type = my_object.properties["type"]
330 if enemy_type == "robot":
331 enemy = RobotEnemy()
332 elif enemy_type == "zombie":
333 enemy = ZombieEnemy()
334 enemy.center_x = math.floor(
335 cartesian[0] * TILE_SCALING * self.tile_map.tile_width
336 )
337 enemy.center_y = math.floor(
338 (cartesian[1] + 1) * (self.tile_map.tile_height * TILE_SCALING)
339 )
340 if "boundary_left" in my_object.properties:
341 enemy.boundary_left = my_object.properties["boundary_left"]
342 if "boundary_right" in my_object.properties:
343 enemy.boundary_right = my_object.properties["boundary_right"]
344 if "change_x" in my_object.properties:
345 enemy.change_x = my_object.properties["change_x"]
346 self.scene.add_sprite(LAYER_NAME_ENEMIES, enemy)
347
348 # Add bullet spritelist to Scene
349 self.scene.add_sprite_list(LAYER_NAME_BULLETS)
350
351 # --- Other stuff
352 # Set the background color
353 if self.tile_map.background_color:
354 self.background_color = self.tile_map.background_color
355
356 # Create the 'physics engine'
357 self.physics_engine = arcade.PhysicsEnginePlatformer(
358 self.player_sprite,
359 platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS],
360 gravity_constant=GRAVITY,
361 ladders=self.scene[LAYER_NAME_LADDERS],
362 walls=self.scene[LAYER_NAME_PLATFORMS],
363 )
364
365 def on_draw(self):
366 """Render the screen."""
367
368 # Clear the screen to the background color
369 self.clear()
370
371 # Activate the game camera
372 self.camera.use()
373
374 # Draw our Scene
375 self.scene.draw()
376
377 # Activate the GUI camera before drawing GUI elements
378 self.gui_camera.use()
379
380 # Draw our score on the screen, scrolling it with the viewport
381 score_text = f"Score: {self.score}"
382 arcade.draw_text(
383 score_text,
384 10,
385 10,
386 arcade.csscolor.BLACK,
387 18,
388 )
389
390 # Draw hit boxes.
391 # for wall in self.wall_list:
392 # wall.draw_hit_box(arcade.color.BLACK, 3)
393 #
394 # self.player_sprite.draw_hit_box(arcade.color.RED, 3)
395
396 def process_keychange(self):
397 """
398 Called when we change a key up/down, or we move on/off a ladder.
399 """
400 # Process up/down
401 if self.up_pressed and not self.down_pressed:
402 if self.physics_engine.is_on_ladder():
403 self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
404 elif (
405 self.physics_engine.can_jump(y_distance=10)
406 and not self.jump_needs_reset
407 ):
408 self.player_sprite.change_y = PLAYER_JUMP_SPEED
409 self.jump_needs_reset = True
410 arcade.play_sound(self.jump_sound)
411 elif self.down_pressed and not self.up_pressed:
412 if self.physics_engine.is_on_ladder():
413 self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
414
415 # Process up/down when on a ladder and no movement
416 if self.physics_engine.is_on_ladder():
417 if not self.up_pressed and not self.down_pressed:
418 self.player_sprite.change_y = 0
419 elif self.up_pressed and self.down_pressed:
420 self.player_sprite.change_y = 0
421
422 # Process left/right
423 if self.right_pressed and not self.left_pressed:
424 self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
425 elif self.left_pressed and not self.right_pressed:
426 self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
427 else:
428 self.player_sprite.change_x = 0
429
430 def on_key_press(self, key, modifiers):
431 """Called whenever a key is pressed."""
432
433 if key == arcade.key.UP or key == arcade.key.W:
434 self.up_pressed = True
435 elif key == arcade.key.DOWN or key == arcade.key.S:
436 self.down_pressed = True
437 elif key == arcade.key.LEFT or key == arcade.key.A:
438 self.left_pressed = True
439 elif key == arcade.key.RIGHT or key == arcade.key.D:
440 self.right_pressed = True
441
442 if key == arcade.key.Q:
443 self.shoot_pressed = True
444
445 self.process_keychange()
446
447 def on_key_release(self, key, modifiers):
448 """Called when the user releases a key."""
449
450 if key == arcade.key.UP or key == arcade.key.W:
451 self.up_pressed = False
452 self.jump_needs_reset = False
453 elif key == arcade.key.DOWN or key == arcade.key.S:
454 self.down_pressed = False
455 elif key == arcade.key.LEFT or key == arcade.key.A:
456 self.left_pressed = False
457 elif key == arcade.key.RIGHT or key == arcade.key.D:
458 self.right_pressed = False
459
460 if key == arcade.key.Q:
461 self.shoot_pressed = False
462
463 self.process_keychange()
464
465 def center_camera_to_player(self, speed=0.2):
466 screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2)
467 screen_center_y = self.player_sprite.center_y - (
468 self.camera.viewport_height / 2
469 )
470 if screen_center_x < 0:
471 screen_center_x = 0
472 if screen_center_y < 0:
473 screen_center_y = 0
474 player_centered = screen_center_x, screen_center_y
475
476 self.camera.move_to(player_centered, speed)
477
478 def on_update(self, delta_time):
479 """Movement and game logic"""
480
481 # Move the player with the physics engine
482 self.physics_engine.update()
483
484 # Update animations
485 if self.physics_engine.can_jump():
486 self.player_sprite.can_jump = False
487 else:
488 self.player_sprite.can_jump = True
489
490 if self.physics_engine.is_on_ladder() and not self.physics_engine.can_jump():
491 self.player_sprite.is_on_ladder = True
492 self.process_keychange()
493 else:
494 self.player_sprite.is_on_ladder = False
495 self.process_keychange()
496
497 if self.can_shoot:
498 if self.shoot_pressed:
499 arcade.play_sound(self.shoot_sound)
500 bullet = arcade.Sprite(
501 ":resources:images/space_shooter/laserBlue01.png",
502 SPRITE_SCALING_LASER,
503 )
504
505 if self.player_sprite.facing_direction == RIGHT_FACING:
506 bullet.change_x = BULLET_SPEED
507 else:
508 bullet.change_x = -BULLET_SPEED
509
510 bullet.center_x = self.player_sprite.center_x
511 bullet.center_y = self.player_sprite.center_y
512
513 self.scene.add_sprite(LAYER_NAME_BULLETS, bullet)
514
515 self.can_shoot = False
516 else:
517 self.shoot_timer += 1
518 if self.shoot_timer == SHOOT_SPEED:
519 self.can_shoot = True
520 self.shoot_timer = 0
521
522 # Update Animations
523 self.scene.update_animation(
524 delta_time,
525 [
526 LAYER_NAME_COINS,
527 LAYER_NAME_BACKGROUND,
528 LAYER_NAME_PLAYER,
529 LAYER_NAME_ENEMIES,
530 ],
531 )
532
533 # Update moving platforms, enemies, and bullets
534 self.scene.update(
535 [LAYER_NAME_MOVING_PLATFORMS, LAYER_NAME_ENEMIES, LAYER_NAME_BULLETS]
536 )
537
538 # See if the enemy hit a boundary and needs to reverse direction.
539 for enemy in self.scene[LAYER_NAME_ENEMIES]:
540 if (
541 enemy.boundary_right
542 and enemy.right > enemy.boundary_right
543 and enemy.change_x > 0
544 ):
545 enemy.change_x *= -1
546
547 if (
548 enemy.boundary_left
549 and enemy.left < enemy.boundary_left
550 and enemy.change_x < 0
551 ):
552 enemy.change_x *= -1
553
554 for bullet in self.scene[LAYER_NAME_BULLETS]:
555 hit_list = arcade.check_for_collision_with_lists(
556 bullet,
557 [
558 self.scene[LAYER_NAME_ENEMIES],
559 self.scene[LAYER_NAME_PLATFORMS],
560 self.scene[LAYER_NAME_MOVING_PLATFORMS],
561 ],
562 )
563
564 if hit_list:
565 bullet.remove_from_sprite_lists()
566
567 for collision in hit_list:
568 if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists:
569 # The collision was with an enemy
570 collision.health -= BULLET_DAMAGE
571
572 if collision.health <= 0:
573 collision.remove_from_sprite_lists()
574 self.score += 100
575
576 # Hit sound
577 arcade.play_sound(self.hit_sound)
578
579 return
580
581 if (bullet.right < 0) or (
582 bullet.left
583 > (self.tile_map.width * self.tile_map.tile_width) * TILE_SCALING
584 ):
585 bullet.remove_from_sprite_lists()
586
587 player_collision_list = arcade.check_for_collision_with_lists(
588 self.player_sprite,
589 [
590 self.scene[LAYER_NAME_COINS],
591 self.scene[LAYER_NAME_ENEMIES],
592 ],
593 )
594
595 # Loop through each coin we hit (if any) and remove it
596 for collision in player_collision_list:
597 if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists:
598 arcade.play_sound(self.game_over)
599 self.setup()
600 return
601 else:
602 # Figure out how many points this coin is worth
603 if "Points" not in collision.properties:
604 print("Warning, collected a coin without a Points property.")
605 else:
606 points = int(collision.properties["Points"])
607 self.score += points
608
609 # Remove the coin
610 collision.remove_from_sprite_lists()
611 arcade.play_sound(self.collect_coin_sound)
612
613 # Position the camera
614 self.center_camera_to_player()
615
616
617def main():
618 """Main function"""
619 window = MyGame()
620 window.setup()
621 arcade.run()
622
623
624if __name__ == "__main__":
625 main()