Space Shooter in Python
Pygame Online · 216 lines · Left and Right, Up or Space to fire
Shoot the marching invaders before they reach your ship.
How it works
A wave of invaders
Four rows of eight. Each invader is a dictionary with its rectangle, colour and points, and each new wave starts a little lower.
def new_wave(): """Four rows of eight invaders. Each wave starts a little lower.""" global invaders, march, step_timer, bullets, bombs, banner invaders = [] for row in range(4): for col in range(8): rect = pygame.Rect(40 + col * 44, 50 + row * 32 + min(wave - 1, 4) * 8, 28, 22) color = YELLOW if row < 2 else LIGHT_BLUE invaders.append({"rect": rect, "color": color, "points": 20 if row < 2 else 10}) march = 1 # 1 = moving right, -1 = moving left step_timer = 0 bullets = [] bombs = [] banner = 90 # frames to show "Wave N" before the invaders move
The march
The whole fleet steps sideways together. When any invader would leave the screen, they all step down and turn around. The fewer invaders are left, the faster they step.
# The fleet steps sideways; at the edge it steps down and turns. # Fewer invaders means faster steps. step_timer += 1 if step_timer >= max(3, len(invaders) // 2 + 4 - wave): step_timer = 0 legs_out = not legs_out at_edge = False for invader in invaders: r = invader["rect"] if r.left + 8 * march < 8 or r.right + 8 * march > WIDTH - 8: at_edge = True for invader in invaders: if at_edge: invader["rect"].y += 12 else: invader["rect"].x += 8 * march if at_edge: march = -march
Shots and hits
Bullets move up every frame. A bullet that touches an invader removes both, scores points and leaves a small spark that fades over a few frames.
for bullet in bullets[:]: bullet.y -= BULLET_SPEED if bullet.bottom < 0: bullets.remove(bullet) continue for invader in invaders: if bullet.colliderect(invader["rect"]): invaders.remove(invader) bullets.remove(bullet) score += invader["points"] * wave sparks.append([invader["rect"].centerx, invader["rect"].centery, 12]) break
Make it yours
Change the code above and press Run again. Some ideas:
- Add shields: blocks between you and the invaders that bombs wear away.
- Let your ship fire two bullets side by side after wave 3.
- Add a mystery ship that crosses the top now and then for bonus points.
- Change
FIRE_DELAYto fire faster or slower.
The whole program
Plain pygame: it runs the same on a computer with Python and pygame installed.
import random import pygame pygame.init() WIDTH, HEIGHT = 480, 360 screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("Space Shooter") clock = pygame.time.Clock() font = pygame.font.Font(None, 28) big_font = pygame.font.Font(None, 60) BG = (16, 18, 22) STAR = (70, 76, 88) YELLOW = (255, 212, 59) BLUE = (48, 105, 152) LIGHT_BLUE = (90, 162, 224) RED = (235, 95, 85) TEXT = (230, 232, 235) SHIP_SPEED = 5 BULLET_SPEED = 8 BOMB_SPEED = 3 FIRE_DELAY = 18 # frames between your shots START_KEYS = (pygame.K_UP, pygame.K_SPACE, pygame.K_RETURN) ship = pygame.Rect(WIDTH // 2 - 15, HEIGHT - 34, 30, 18) stars = [(random.randint(0, WIDTH), random.randint(0, HEIGHT)) for _ in range(50)] def new_wave(): """Four rows of eight invaders. Each wave starts a little lower.""" global invaders, march, step_timer, bullets, bombs, banner invaders = [] for row in range(4): for col in range(8): rect = pygame.Rect(40 + col * 44, 50 + row * 32 + min(wave - 1, 4) * 8, 28, 22) color = YELLOW if row < 2 else LIGHT_BLUE invaders.append({"rect": rect, "color": color, "points": 20 if row < 2 else 10}) march = 1 # 1 = moving right, -1 = moving left step_timer = 0 bullets = [] bombs = [] banner = 90 # frames to show "Wave N" before the invaders move def new_game(): global wave, score, lives, invincible, sparks wave = 1 score = 0 lives = 3 invincible = 0 # frames left of blinking after a hit sparks = [] # [x, y, frames left] for each small explosion new_wave() def draw_invader(invader): r = invader["rect"] color = invader["color"] pygame.draw.rect(screen, color, (r.x, r.y, r.width, r.height - 6), border_radius=6) pygame.draw.rect(screen, BG, (r.x + 7, r.y + 5, 4, 4)) # eyes pygame.draw.rect(screen, BG, (r.right - 11, r.y + 5, 4, 4)) spread = 2 if legs_out else 6 # the legs move with every step pygame.draw.rect(screen, color, (r.x + spread, r.bottom - 6, 4, 6)) pygame.draw.rect(screen, color, (r.right - spread - 4, r.bottom - 6, 4, 6)) def draw_text(text, text_font, color, center): image = text_font.render(text, True, color) screen.blit(image, image.get_rect(center=center)) new_game() state = "ready" # "ready", "playing" or "over" legs_out = False fire_timer = 0 wait = 0 # frames until a new game may start running = True while running: fire = False for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN and event.key in START_KEYS: if state == "ready": state = "playing" elif state == "over" and wait == 0: new_game() state = "playing" elif state == "playing": fire = True if wait > 0: wait -= 1 if fire_timer > 0: fire_timer -= 1 if state == "playing": keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: ship.x -= SHIP_SPEED if keys[pygame.K_RIGHT]: ship.x += SHIP_SPEED ship.clamp_ip(screen.get_rect()) # Hold Up or Space to keep firing. if (fire or keys[pygame.K_UP] or keys[pygame.K_SPACE]) and fire_timer == 0: bullets.append(pygame.Rect(ship.centerx - 2, ship.top - 10, 4, 10)) fire_timer = FIRE_DELAY for bullet in bullets[:]: bullet.y -= BULLET_SPEED if bullet.bottom < 0: bullets.remove(bullet) continue for invader in invaders: if bullet.colliderect(invader["rect"]): invaders.remove(invader) bullets.remove(bullet) score += invader["points"] * wave sparks.append([invader["rect"].centerx, invader["rect"].centery, 12]) break if not invaders: wave += 1 new_wave() elif banner > 0: banner -= 1 else: # The fleet steps sideways; at the edge it steps down and turns. # Fewer invaders means faster steps. step_timer += 1 if step_timer >= max(3, len(invaders) // 2 + 4 - wave): step_timer = 0 legs_out = not legs_out at_edge = False for invader in invaders: r = invader["rect"] if r.left + 8 * march < 8 or r.right + 8 * march > WIDTH - 8: at_edge = True for invader in invaders: if at_edge: invader["rect"].y += 12 else: invader["rect"].x += 8 * march if at_edge: march = -march # Now and then a random invader drops a bomb. if random.random() < 0.01 + 0.004 * wave and len(bombs) < 2 + wave: shooter = random.choice(invaders)["rect"] bombs.append(pygame.Rect(shooter.centerx - 2, shooter.bottom, 4, 10)) for bomb in bombs[:]: bomb.y += BOMB_SPEED if bomb.top > HEIGHT: bombs.remove(bomb) elif bomb.colliderect(ship) and invincible == 0: lives -= 1 invincible = 90 bombs.clear() sparks.append([ship.centerx, ship.centery, 12]) break if invincible > 0: invincible -= 1 # Invaders that reach your ship win straight away. for invader in invaders: if invader["rect"].bottom >= ship.top: lives = 0 if lives <= 0: state = "over" wait = 40 screen.fill(BG) for star in stars: pygame.draw.circle(screen, STAR, star, 1) for invader in invaders: draw_invader(invader) for bullet in bullets: pygame.draw.rect(screen, YELLOW, bullet) for bomb in bombs: pygame.draw.rect(screen, RED, bomb) for spark in sparks[:]: pygame.draw.circle(screen, YELLOW, spark[:2], 16 - spark[2], 2) spark[2] -= 1 if spark[2] == 0: sparks.remove(spark) if state != "over" and invincible // 6 % 2 == 0: # blink after a hit pygame.draw.polygon(screen, YELLOW, [ship.midtop, ship.bottomleft, ship.bottomright]) screen.blit(font.render(f"Score {score}", True, TEXT), (12, 12)) draw_text(f"Wave {wave}", font, LIGHT_BLUE, (WIDTH // 2, 21)) for i in range(lives): x = WIDTH - 24 - i * 22 pygame.draw.polygon(screen, YELLOW, [(x, 12), (x - 8, 28), (x + 8, 28)]) if state == "playing" and banner > 0: draw_text(f"Wave {wave}", big_font, YELLOW, (WIDTH // 2, 230)) elif state != "playing": panel = pygame.Rect(0, 0, 380, 140) panel.center = (WIDTH // 2, 200) pygame.draw.rect(screen, BG, panel, border_radius=12) pygame.draw.rect(screen, BLUE, panel, 2, border_radius=12) if state == "ready": draw_text("Space Shooter", big_font, YELLOW, (panel.centerx, panel.y + 38)) draw_text("Left and Right move, Up or Space fires", font, TEXT, (panel.centerx, panel.y + 82)) draw_text("Press Up to start", font, TEXT, (panel.centerx, panel.y + 112)) else: draw_text("Game over", big_font, YELLOW, (panel.centerx, panel.y + 42)) draw_text(f"Score {score}", font, TEXT, (panel.centerx, panel.y + 82)) draw_text("Press Up or Space to play again", font, TEXT, (panel.centerx, panel.y + 112)) pygame.display.flip() clock.tick(60) pygame.quit()