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