Asteroid Smasher
This is a sample asteroid smasher game made with the Arcade library.
asteroid_smasher.py
1"""
2Asteroid Smasher
3
4Shoot space rocks in this demo program created with Python and the
5Arcade library.
6
7Artwork from https://kenney.nl
8
9For a fancier example of this game, see:
10https://github.com/pythonarcade/asteroids
11
12If Python and Arcade are installed, this example can be run from
13the command line with:
14python -m arcade.examples.asteroid_smasher
15"""
16import random
17import math
18import arcade
19
20from typing import cast
21
22WINDOW_TITLE = "Asteroid Smasher"
23STARTING_ASTEROID_COUNT = 3
24SCALE = 0.5
25
26# Screen dimensions and limits
27WINDOW_WIDTH = 1280
28WINDOW_HEIGHT = 720
29OFFSCREEN_SPACE = 300
30LEFT_LIMIT = -OFFSCREEN_SPACE
31RIGHT_LIMIT = WINDOW_WIDTH + OFFSCREEN_SPACE
32BOTTOM_LIMIT = -OFFSCREEN_SPACE
33TOP_LIMIT = WINDOW_HEIGHT + OFFSCREEN_SPACE
34
35# Control player speed
36TURN_SPEED = 3
37THRUST_AMOUNT = 0.2
38
39# Asteroid types
40ASTERIOD_TYPE_BIG = 4
41ASTERIOD_TYPE_MEDIUM = 3
42ASTERIOD_TYPE_SMALL = 2
43ASTERIOD_TYPE_TINY = 1
44
45
46class TurningSprite(arcade.Sprite):
47 """ Sprite that sets its angle to the direction it is traveling in. """
48 def update(self, delta_time=1 / 60):
49 """ Move the sprite """
50 super().update(delta_time)
51 self.angle = -math.degrees(math.atan2(self.change_y, self.change_x))
52
53
54class ShipSprite(arcade.Sprite):
55 """ Sprite that represents our spaceship. """
56 def __init__(self, filename, scale):
57 """ Set up the spaceship. """
58
59 # Call the parent Sprite constructor
60 super().__init__(filename, scale=scale)
61
62 # Info on the space ship.
63 # Angle comes in automatically from the parent class.
64 self.thrust = 0
65 self.speed = 0
66 self.max_speed = 4
67 self.drag = 0.05
68 self.respawning = 0
69
70 # Mark that we are respawning.
71 self.respawn()
72
73 def respawn(self):
74 """
75 Called when we die and need to make a new ship.
76 'respawning' is an invulnerability timer.
77 """
78 # If we are in the middle of respawning, this is non-zero.
79 self.respawning = 1
80 self.alpha = 0
81 self.center_x = WINDOW_WIDTH / 2
82 self.center_y = WINDOW_HEIGHT / 2
83 self.angle = 0
84
85 def update(self, delta_time=1 / 60):
86 """ Update our position and other particulars. """
87
88 # Is the user spawning
89 if self.respawning:
90 # Increase spawn counter, setting alpha to that amount
91 self.respawning += 1
92 self.alpha = self.respawning
93 # Once we are close enough, set alpha to 255 and clear
94 # respawning flag
95 if self.respawning > 230:
96 self.respawning = 0
97 self.alpha = 255
98
99 # Apply drag forward
100 if self.speed > 0:
101 self.speed -= self.drag
102 if self.speed < 0:
103 self.speed = 0
104 # Apply drag reverse
105 if self.speed < 0:
106 self.speed += self.drag
107 if self.speed > 0:
108 self.speed = 0
109
110 # Apply thrust
111 self.speed += self.thrust
112
113 # Enforce speed limit
114 if self.speed > self.max_speed:
115 self.speed = self.max_speed
116 if self.speed < -self.max_speed:
117 self.speed = -self.max_speed
118
119 # Calculate movement vector based on speed/angle
120 self.change_x = math.sin(math.radians(self.angle)) * self.speed
121 self.change_y = math.cos(math.radians(self.angle)) * self.speed
122
123 # Apply movement vector
124 self.center_x += self.change_x
125 self.center_y += self.change_y
126
127 # If the ship goes off-screen, move it to the other side of the window
128 if self.right < 0:
129 self.left = WINDOW_WIDTH
130 if self.left > WINDOW_WIDTH:
131 self.right = 0
132 if self.bottom < 0:
133 self.top = WINDOW_HEIGHT
134 if self.top > WINDOW_HEIGHT:
135 self.bottom = 0
136
137 """ Call the parent class. """
138 super().update()
139
140
141class AsteroidSprite(arcade.Sprite):
142 """Sprite that represents an asteroid."""
143
144 def __init__(self, image_file_name, scale, type):
145 super().__init__(image_file_name, scale=scale)
146 self.type = type
147
148 def update(self, delta_time=1 / 60):
149 """ Move the asteroid around. """
150 super().update(delta_time)
151 if self.center_x < LEFT_LIMIT:
152 self.center_x = RIGHT_LIMIT
153 if self.center_x > RIGHT_LIMIT:
154 self.center_x = LEFT_LIMIT
155 if self.center_y > TOP_LIMIT:
156 self.center_y = BOTTOM_LIMIT
157 if self.center_y < BOTTOM_LIMIT:
158 self.center_y = TOP_LIMIT
159
160
161class GameView(arcade.View):
162 """ Main application class. """
163
164 def __init__(self):
165 super().__init__()
166
167 self.game_over = False
168
169 # Create sprite lists
170 self.player_sprite_list = arcade.SpriteList()
171 self.asteroid_list = arcade.SpriteList()
172 self.bullet_list = arcade.SpriteList()
173 self.ship_life_list = arcade.SpriteList()
174
175 # Set up the player
176 self.score = 0
177 self.player_sprite = None
178 self.lives = 3
179
180 # Load sounds
181 self.laser_sound = arcade.load_sound(":resources:sounds/hurt5.wav")
182 self.hit_sound1 = arcade.load_sound(":resources:sounds/explosion1.wav")
183 self.hit_sound2 = arcade.load_sound(":resources:sounds/explosion2.wav")
184 self.hit_sound3 = arcade.load_sound(":resources:sounds/hit1.wav")
185 self.hit_sound4 = arcade.load_sound(":resources:sounds/hit2.wav")
186
187 # Text fields
188 self.text_score = arcade.Text(
189 f"Score: {self.score}",
190 x=10,
191 y=70,
192 font_size=13,
193 )
194 self.text_asteroid_count = arcade.Text(
195 f"Asteroid Count: {len(self.asteroid_list)}",
196 x=10,
197 y=50,
198 font_size=13,
199 )
200
201 def start_new_game(self):
202 """ Set up the game and initialize the variables. """
203
204 self.game_over = False
205
206 # Sprite lists
207 self.player_sprite_list = arcade.SpriteList()
208 self.asteroid_list = arcade.SpriteList()
209 self.bullet_list = arcade.SpriteList()
210 self.ship_life_list = arcade.SpriteList()
211
212 # Set up the player
213 self.score = 0
214 self.player_sprite = ShipSprite(
215 ":resources:images/space_shooter/playerShip1_orange.png",
216 scale=SCALE,
217 )
218 self.player_sprite_list.append(self.player_sprite)
219 self.lives = 3
220
221 # Set up the little icons that represent the player lives.
222 cur_pos = 10
223 for i in range(self.lives):
224 life = arcade.Sprite(
225 ":resources:images/space_shooter/playerLife1_orange.png",
226 scale=SCALE,
227 )
228 life.center_x = cur_pos + life.width
229 life.center_y = life.height
230 cur_pos += life.width
231 self.ship_life_list.append(life)
232
233 # Make the asteroids
234 image_list = (
235 ":resources:images/space_shooter/meteorGrey_big1.png",
236 ":resources:images/space_shooter/meteorGrey_big2.png",
237 ":resources:images/space_shooter/meteorGrey_big3.png",
238 ":resources:images/space_shooter/meteorGrey_big4.png",
239 )
240 for i in range(STARTING_ASTEROID_COUNT):
241 # Pick one of four random rock images
242 image_no = random.randrange(4)
243
244 enemy_sprite = AsteroidSprite(
245 image_list[image_no],
246 scale=SCALE,
247 type=ASTERIOD_TYPE_BIG,
248 )
249
250 # Set position
251 enemy_sprite.center_y = random.randrange(BOTTOM_LIMIT, TOP_LIMIT)
252 enemy_sprite.center_x = random.randrange(LEFT_LIMIT, RIGHT_LIMIT)
253
254 # Set speed / rotation
255 enemy_sprite.change_x = random.random() * 2 - 1
256 enemy_sprite.change_y = random.random() * 2 - 1
257 enemy_sprite.change_angle = (random.random() - 0.5) * 2
258
259 self.asteroid_list.append(enemy_sprite)
260
261 self.text_score.text = f"Score: {self.score}"
262 self.text_asteroid_count.text = f"Asteroid Count: {len(self.asteroid_list)}"
263
264 def on_draw(self):
265 """ Render the screen """
266
267 # Clear the screen before we start drawing
268 self.clear()
269
270 # Draw all the sprites.
271 self.asteroid_list.draw()
272 self.ship_life_list.draw()
273 self.bullet_list.draw()
274 self.player_sprite_list.draw()
275
276 # Draw the text
277 self.text_score.draw()
278 self.text_asteroid_count.draw()
279
280 def on_key_press(self, symbol, modifiers):
281 """ Called whenever a key is pressed. """
282 # Shoot if the player hit the space bar and we aren't respawning.
283 if not self.player_sprite.respawning and symbol == arcade.key.SPACE:
284 bullet_sprite = TurningSprite(":resources:images/space_shooter/laserBlue01.png",
285 scale=SCALE)
286
287 # Set bullet vector
288 bullet_speed = 13
289 angle_radians = math.radians(self.player_sprite.angle)
290 bullet_sprite.change_y = math.cos(angle_radians) * bullet_speed
291 bullet_sprite.change_x = math.sin(angle_radians) * bullet_speed
292
293 # Set bullet position
294 bullet_sprite.center_x = self.player_sprite.center_x
295 bullet_sprite.center_y = self.player_sprite.center_y
296
297 # Add to our sprite list
298 self.bullet_list.append(bullet_sprite)
299
300 # Go ahead and move it a frame
301 bullet_sprite.update()
302
303 # Pew pew
304 arcade.play_sound(self.laser_sound, speed=random.random() * 3 + 0.5)
305
306 if symbol == arcade.key.LEFT:
307 self.player_sprite.change_angle = -TURN_SPEED
308 elif symbol == arcade.key.RIGHT:
309 self.player_sprite.change_angle = TURN_SPEED
310 elif symbol == arcade.key.UP:
311 self.player_sprite.thrust = THRUST_AMOUNT
312 elif symbol == arcade.key.DOWN:
313 self.player_sprite.thrust = -THRUST_AMOUNT
314 # Restart the game if the player hits 'R'
315 elif symbol == arcade.key.R:
316 self.start_new_game()
317 # Quit if the player hits escape
318 elif symbol == arcade.key.ESCAPE:
319 self.window.close()
320
321 def on_key_release(self, symbol, modifiers):
322 """ Called whenever a key is released. """
323 if symbol == arcade.key.LEFT:
324 self.player_sprite.change_angle = 0
325 elif symbol == arcade.key.RIGHT:
326 self.player_sprite.change_angle = 0
327 elif symbol == arcade.key.UP:
328 self.player_sprite.thrust = 0
329 elif symbol == arcade.key.DOWN:
330 self.player_sprite.thrust = 0
331
332 def split_asteroid(self, asteroid: AsteroidSprite):
333 """ Split an asteroid into chunks. """
334 x = asteroid.center_x
335 y = asteroid.center_y
336 self.score += 1
337
338 if asteroid.type == ASTERIOD_TYPE_BIG:
339 # Split large asteroid into 2 medium ones
340 for i in range(3):
341 image_no = random.randrange(2)
342 image_list = [":resources:images/space_shooter/meteorGrey_med1.png",
343 ":resources:images/space_shooter/meteorGrey_med2.png"]
344
345 enemy_sprite = AsteroidSprite(image_list[image_no],
346 scale=SCALE * 1.5,
347 type=ASTERIOD_TYPE_MEDIUM)
348
349 enemy_sprite.center_y = y
350 enemy_sprite.center_x = x
351
352 enemy_sprite.change_x = random.random() * 2.5 - 1.25
353 enemy_sprite.change_y = random.random() * 2.5 - 1.25
354
355 enemy_sprite.change_angle = (random.random() - 0.5) * 2
356
357 self.asteroid_list.append(enemy_sprite)
358 self.hit_sound1.play()
359
360 elif asteroid.type == ASTERIOD_TYPE_MEDIUM:
361 # Split medium asteroid into 2 small ones
362 for i in range(3):
363 image_no = random.randrange(2)
364 image_list = [":resources:images/space_shooter/meteorGrey_small1.png",
365 ":resources:images/space_shooter/meteorGrey_small2.png"]
366
367 enemy_sprite = AsteroidSprite(image_list[image_no],
368 scale=SCALE * 1.5,
369 type=ASTERIOD_TYPE_SMALL)
370
371 enemy_sprite.center_y = y
372 enemy_sprite.center_x = x
373
374 enemy_sprite.change_x = random.random() * 3 - 1.5
375 enemy_sprite.change_y = random.random() * 3 - 1.5
376
377 enemy_sprite.change_angle = (random.random() - 0.5) * 2
378
379 self.asteroid_list.append(enemy_sprite)
380 self.hit_sound2.play()
381
382 elif asteroid.type == ASTERIOD_TYPE_SMALL:
383 # Split small asteroid into 2 tiny ones
384 for i in range(3):
385 image_no = random.randrange(2)
386 image_list = [":resources:images/space_shooter/meteorGrey_tiny1.png",
387 ":resources:images/space_shooter/meteorGrey_tiny2.png"]
388
389 enemy_sprite = AsteroidSprite(image_list[image_no],
390 scale=SCALE * 1.5,
391 type=ASTERIOD_TYPE_TINY)
392
393 enemy_sprite.center_y = y
394 enemy_sprite.center_x = x
395
396 enemy_sprite.change_x = random.random() * 3.5 - 1.75
397 enemy_sprite.change_y = random.random() * 3.5 - 1.75
398
399 enemy_sprite.change_angle = (random.random() - 0.5) * 2
400
401 self.asteroid_list.append(enemy_sprite)
402 self.hit_sound3.play()
403
404 elif asteroid.type == ASTERIOD_TYPE_TINY:
405 # Do nothing. The tiny asteroid just goes away.
406 self.hit_sound4.play()
407
408 def on_update(self, x):
409 """ Move everything """
410
411 if not self.game_over:
412 self.asteroid_list.update()
413 self.bullet_list.update()
414 self.player_sprite_list.update()
415
416 for bullet in self.bullet_list:
417 asteroids = arcade.check_for_collision_with_list(bullet,
418 self.asteroid_list)
419
420 for asteroid in asteroids:
421 # expected AsteroidSprite, got Sprite instead
422 self.split_asteroid(cast(AsteroidSprite, asteroid))
423 asteroid.remove_from_sprite_lists()
424 bullet.remove_from_sprite_lists()
425
426 # Remove bullet if it goes off-screen
427 size = max(bullet.width, bullet.height)
428 if bullet.center_x < 0 - size:
429 bullet.remove_from_sprite_lists()
430 if bullet.center_x > WINDOW_WIDTH + size:
431 bullet.remove_from_sprite_lists()
432 if bullet.center_y < 0 - size:
433 bullet.remove_from_sprite_lists()
434 if bullet.center_y > WINDOW_HEIGHT + size:
435 bullet.remove_from_sprite_lists()
436
437 if not self.player_sprite.respawning:
438 asteroids = arcade.check_for_collision_with_list(self.player_sprite,
439 self.asteroid_list)
440 if len(asteroids) > 0:
441 if self.lives > 0:
442 self.lives -= 1
443 self.player_sprite.respawn()
444 self.split_asteroid(cast(AsteroidSprite, asteroids[0]))
445 asteroids[0].remove_from_sprite_lists()
446 self.ship_life_list.pop().remove_from_sprite_lists()
447 print("Crash")
448 else:
449 self.game_over = True
450 print("Game over")
451
452 # Update the text objects
453 self.text_score.text = f"Score: {self.score}"
454 self.text_asteroid_count.text = f"Asteroid Count: {len(self.asteroid_list)}"
455
456
457def main():
458 """ Main function """
459 # Create a window class. This is what actually shows up on screen
460 window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
461
462 # Create and setup the GameView
463 game = GameView()
464 game.start_new_game()
465
466 # Show GameView on screen
467 window.show_view(game)
468
469 # Start the arcade game loop
470 arcade.run()
471
472
473if __name__ == "__main__":
474 main()