Editor

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_DELAY to 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()