Chapter 2.5: Building Space Invaders

Classic Arcade Action with Sprites and Matrices

๐ŸŽฎ PROJECT 2.5 | Difficulty: Intermediate-Advanced | Time: 30 minutes

๐Ÿ’ป Interactive Options:

๐Ÿ‘พ Game Design: Space Invaders

Goal: Defend Earth by shooting alien invaders before they reach the bottom

Game Mechanics:

  • Player ship moves left/right at bottom
  • Shoot bullets upward
  • Aliens move side-to-side and down
  • Aliens shoot back!
  • Game over if aliens reach bottom or player is hit

๐ŸŽจ Sprites Using Matrices

Weโ€™ll represent sprites as 2D matrices (like pixel art):

import pygame
import numpy as np

# Define alien as a matrix (1 = filled, 0 = empty)
ALIEN_SPRITE = [
    [0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
    [0, 1, 1, 0, 1, 1, 0, 1, 1, 0],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 1, 1, 1, 1, 1, 1, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1, 0, 1],
    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0],
]

PLAYER_SPRITE = [
    [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
    [0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]

def draw_sprite(surface, sprite, x, y, color, pixel_size=3):
    """Draw a sprite matrix on the screen"""
    for row_idx, row in enumerate(sprite):
        for col_idx, pixel in enumerate(row):
            if pixel:
                rect = pygame.Rect(
                    x + col_idx * pixel_size,
                    y + row_idx * pixel_size,
                    pixel_size,
                    pixel_size
                )
                pygame.draw.rect(surface, color, rect)

print("โœ… Sprite system defined!")
print(f"   Alien: {len(ALIEN_SPRITE)}x{len(ALIEN_SPRITE[0])} matrix")
print(f"   Player: {len(PLAYER_SPRITE)}x{len(PLAYER_SPRITE[0])} matrix")

๐Ÿš€ Complete Space Invaders Game

# space_invaders.py
import pygame
import random
import sys

pygame.init()

# Constants
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60

# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
YELLOW = (255, 255, 0)

# Sprites
ALIEN_SPRITE = [
    [0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
    [0, 1, 1, 0, 1, 1, 0, 1, 1, 0],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 1, 1, 1, 1, 1, 1, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1, 0, 1],
    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0],
]

PLAYER_SPRITE = [
    [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
    [0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]

screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Space Invaders")
clock = pygame.time.Clock()

def draw_sprite(surface, sprite, x, y, color, pixel_size=3):
    for row_idx, row in enumerate(sprite):
        for col_idx, pixel in enumerate(row):
            if pixel:
                rect = pygame.Rect(
                    x + col_idx * pixel_size,
                    y + row_idx * pixel_size,
                    pixel_size,
                    pixel_size
                )
                pygame.draw.rect(surface, color, rect)

class Player:
    def __init__(self):
        self.x = SCREEN_WIDTH // 2
        self.y = SCREEN_HEIGHT - 80
        self.speed = 5
        self.width = 30
        self.height = 18
    
    def move(self, dx):
        self.x += dx * self.speed
        self.x = max(0, min(self.x, SCREEN_WIDTH - self.width))
    
    def draw(self, surface):
        draw_sprite(surface, PLAYER_SPRITE, self.x, self.y, GREEN)
    
    def shoot(self):
        return Bullet(self.x + self.width // 2, self.y, -1)

class Alien:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.width = 30
        self.height = 24
    
    def draw(self, surface):
        draw_sprite(surface, ALIEN_SPRITE, self.x, self.y, RED)
    
    def move(self, dx, dy):
        self.x += dx
        self.y += dy

class Bullet:
    def __init__(self, x, y, direction):
        self.x = x
        self.y = y
        self.direction = direction  # -1 for up, 1 for down
        self.speed = 7
        self.width = 3
        self.height = 10
        self.active = True
    
    def update(self):
        self.y += self.direction * self.speed
        if self.y < 0 or self.y > SCREEN_HEIGHT:
            self.active = False
    
    def draw(self, surface):
        color = YELLOW if self.direction == -1 else RED
        pygame.draw.rect(surface, color, (self.x, self.y, self.width, self.height))
    
    def get_rect(self):
        return pygame.Rect(self.x, self.y, self.width, self.height)

class Game:
    def __init__(self):
        self.player = Player()
        self.aliens = []
        self.player_bullets = []
        self.alien_bullets = []
        self.score = 0
        self.game_over = False
        self.alien_direction = 1
        self.alien_speed = 1
        self.shoot_cooldown = 0
        self.create_aliens()
    
    def create_aliens(self):
        rows = 4
        cols = 10
        spacing_x = 60
        spacing_y = 50
        start_x = 100
        start_y = 50
        
        for row in range(rows):
            for col in range(cols):
                x = start_x + col * spacing_x
                y = start_y + row * spacing_y
                self.aliens.append(Alien(x, y))
    
    def handle_input(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE and self.shoot_cooldown == 0:
                    self.player_bullets.append(self.player.shoot())
                    self.shoot_cooldown = 20
                elif event.key == pygame.K_r and self.game_over:
                    self.__init__()
        
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.player.move(-1)
        if keys[pygame.K_RIGHT]:
            self.player.move(1)
        
        return True
    
    def update(self):
        if self.game_over:
            return
        
        if self.shoot_cooldown > 0:
            self.shoot_cooldown -= 1
        
        # Move aliens
        move_down = False
        for alien in self.aliens:
            alien.move(self.alien_direction * self.alien_speed, 0)
            if alien.x <= 0 or alien.x >= SCREEN_WIDTH - alien.width:
                move_down = True
        
        if move_down:
            self.alien_direction *= -1
            for alien in self.aliens:
                alien.y += 20
                if alien.y >= SCREEN_HEIGHT - 100:
                    self.game_over = True
        
        # Update bullets
        for bullet in self.player_bullets:
            bullet.update()
        self.player_bullets = [b for b in self.player_bullets if b.active]
        
        for bullet in self.alien_bullets:
            bullet.update()
        self.alien_bullets = [b for b in self.alien_bullets if b.active]
        
        # Alien shooting
        if random.random() < 0.01 and self.aliens:
            shooter = random.choice(self.aliens)
            self.alien_bullets.append(
                Bullet(shooter.x + shooter.width // 2, shooter.y + shooter.height, 1)
            )
        
        # Check collisions
        for bullet in self.player_bullets[:]:
            bullet_rect = bullet.get_rect()
            for alien in self.aliens[:]:
                alien_rect = pygame.Rect(alien.x, alien.y, alien.width, alien.height)
                if bullet_rect.colliderect(alien_rect):
                    self.aliens.remove(alien)
                    bullet.active = False
                    self.score += 10
                    break
        
        # Check player hit
        player_rect = pygame.Rect(self.player.x, self.player.y, 
                                  self.player.width, self.player.height)
        for bullet in self.alien_bullets:
            if bullet.get_rect().colliderect(player_rect):
                self.game_over = True
        
        # Win condition
        if not self.aliens:
            self.game_over = True
    
    def draw(self):
        screen.fill(BLACK)
        
        self.player.draw(screen)
        
        for alien in self.aliens:
            alien.draw(screen)
        
        for bullet in self.player_bullets:
            bullet.draw(screen)
        
        for bullet in self.alien_bullets:
            bullet.draw(screen)
        
        # UI
        font = pygame.font.Font(None, 36)
        score_text = font.render(f'Score: {self.score}', True, WHITE)
        screen.blit(score_text, (10, 10))
        
        if self.game_over:
            overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
            overlay.set_alpha(200)
            overlay.fill(BLACK)
            screen.blit(overlay, (0, 0))
            
            game_over_font = pygame.font.Font(None, 72)
            if not self.aliens:
                text = game_over_font.render('YOU WIN!', True, GREEN)
            else:
                text = game_over_font.render('GAME OVER', True, RED)
            
            screen.blit(text, (SCREEN_WIDTH // 2 - text.get_width() // 2, 250))
            
            restart_font = pygame.font.Font(None, 36)
            restart_text = restart_font.render('Press R to Restart', True, WHITE)
            screen.blit(restart_text, (SCREEN_WIDTH // 2 - restart_text.get_width() // 2, 350))
        
        pygame.display.flip()

# Main loop
game = Game()
running = True

while running:
    running = game.handle_input()
    game.update()
    game.draw()
    clock.tick(FPS)

pygame.quit()
sys.exit()

๐ŸŽฏ Key Concepts: Collision Detection

# Rectangle collision detection
def check_collision(rect1, rect2):
    """
    Check if two rectangles overlap
    rect format: (x, y, width, height)
    """
    x1, y1, w1, h1 = rect1
    x2, y2, w2, h2 = rect2
    
    # Check if rectangles DON'T overlap, then negate
    return not (x1 + w1 < x2 or  # rect1 left of rect2
                x2 + w2 < x1 or  # rect2 left of rect1
                y1 + h1 < y2 or  # rect1 above rect2
                y2 + h2 < y1)    # rect2 above rect1

# Examples
bullet = (100, 200, 3, 10)
alien = (95, 190, 30, 24)
miss = (200, 200, 3, 10)

print("๐ŸŽฏ Collision Examples:")
print(f"  Bullet hits alien: {check_collision(bullet, alien)}")
print(f"  Bullet misses: {check_collision(miss, alien)}")

๐Ÿ† Enhancement Challenges

Challenge 1: Shields

Add destructible shields between player and aliens:

  • Create 3-4 shields
  • Bullets destroy shield pixels
  • Shields protect player

Challenge 2: Wave System

Make aliens come in waves:

  • After defeating all aliens, spawn new wave
  • Each wave is faster/harder
  • Track wave number

Challenge 3: Power-Ups

Add power-ups that fall from destroyed aliens:

  • Rapid fire
  • Shield
  • Multi-shot

Challenge 4: Boss Battle

Add a boss alien that:

  • Has more health
  • Moves differently
  • Shoots multiple bullets
  • Appears every 5 waves

๐Ÿš€ Whatโ€™s Next?

Youโ€™ve now built two classic games! In the final slide, weโ€™ll explore:

  • Advanced game patterns
  • Particle effects
  • Game juice (polish)
  • Where to go next in game development