Chapter 13: Aliens

Add enemies. Detect collisions. Implement game over conditions.

The Alien Class

# alien.py
import pygame
from pygame.sprite import Sprite

class Alien(Sprite):
    """Represents a single alien."""

    def __init__(self, ai_game):
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings

        self.image = pygame.image.load('images/alien.bmp')
        self.rect = self.image.get_rect()

        # Start near top-left
        self.rect.x = self.rect.width
        self.rect.y = self.rect.height

        # Store exact horizontal position
        self.x = float(self.rect.x)

    def update(self):
        """Move alien horizontally."""
        self.x += self.settings.alien_speed * self.settings.fleet_direction
        self.rect.x = self.x

    def check_edges(self):
        """Return True if alien at screen edge."""
        screen_rect = self.screen.get_rect()
        return (self.rect.right >= screen_rect.right or
                self.rect.left <= 0)

Creating the Fleet

Calculating Fleet Size

# In settings.py
self.alien_speed = 1.0
self.fleet_drop_speed = 10
self.fleet_direction = 1  # 1 = right, -1 = left
# In AlienInvasion
from alien import Alien

def __init__(self):
    # ... existing code ...
    self.aliens = Group()
    self._create_fleet()

def _create_fleet(self):
    """Create the fleet of aliens."""
    alien = Alien(self)
    alien_width, alien_height = alien.rect.size

    # Calculate available space
    available_space_x = self.settings.screen_width - (2 * alien_width)
    number_aliens_x = available_space_x // (2 * alien_width)

    # Calculate rows
    ship_height = self.ship.rect.height
    available_space_y = (self.settings.screen_height -
                         (3 * alien_height) - ship_height)
    number_rows = available_space_y // (2 * alien_height)

    # Create full fleet
    for row_number in range(number_rows):
        for alien_number in range(number_aliens_x):
            self._create_alien(alien_number, row_number)

def _create_alien(self, alien_number, row_number):
    """Create alien and place in row."""
    alien = Alien(self)
    alien_width, alien_height = alien.rect.size

    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    alien.rect.y = alien_height + 2 * alien_height * row_number

    self.aliens.add(alien)

Fleet Movement

Moving Side to Side

def _check_fleet_edges(self):
    """Respond if any alien hits edge."""
    for alien in self.aliens.sprites():
        if alien.check_edges():
            self._change_fleet_direction()
            break

def _change_fleet_direction(self):
    """Drop fleet and change direction."""
    for alien in self.aliens.sprites():
        alien.rect.y += self.settings.fleet_drop_speed
    self.settings.fleet_direction *= -1  (1)
1 Multiply by -1 reverses direction

Update Game Loop

def run_game(self):
    while True:
        self._check_events()
        self.ship.update()
        self._update_bullets()
        self._update_aliens()  (1)
        self._update_screen()
        self.clock.tick(60)

def _update_aliens(self):
    """Check edges, then update positions."""
    self._check_fleet_edges()
    self.aliens.update()

def _update_screen(self):
    self.screen.fill(self.settings.bg_color)
    for bullet in self.bullets.sprites():
        bullet.draw_bullet()
    self.ship.blitme()
    self.aliens.draw(self.screen)  (2)
    pygame.display.flip()
1 Add alien update to loop
2 Group’s draw() draws all sprites

Collision Detection

Shooting Aliens

def _update_bullets(self):
    self.bullets.update()

    # Remove off-screen bullets
    for bullet in self.bullets.copy():
        if bullet.rect.bottom <= 0:
            self.bullets.remove(bullet)

    self._check_bullet_alien_collisions()

def _check_bullet_alien_collisions(self):
    """Handle bullet-alien collisions."""
    # Check for collisions - returns dict of {bullet: [aliens]}
    collisions = pygame.sprite.groupcollide(
        self.bullets, self.aliens, True, True  (1)
    )

    # Repopulate fleet if empty
    if not self.aliens:
        self.bullets.empty()
        self._create_fleet()
1 Both True = remove both bullet and alien on collision

Ship Hit by Alien

# game_stats.py
class GameStats:
    """Track game statistics."""

    def __init__(self, ai_game):
        self.settings = ai_game.settings
        self.reset_stats()

    def reset_stats(self):
        """Initialize stats that change during game."""
        self.ships_left = self.settings.ship_limit
# In settings.py
self.ship_limit = 3
# In AlienInvasion
from game_stats import GameStats
import time

def __init__(self):
    # ... existing code ...
    self.stats = GameStats(self)
    self.game_active = True

def _update_aliens(self):
    self._check_fleet_edges()
    self.aliens.update()

    # Check ship-alien collision
    if pygame.sprite.spritecollideany(self.ship, self.aliens):
        self._ship_hit()

    # Check aliens reaching bottom
    self._check_aliens_bottom()

def _ship_hit(self):
    """Respond to ship being hit."""
    if self.stats.ships_left > 0:
        self.stats.ships_left -= 1

        # Clear remaining aliens and bullets
        self.aliens.empty()
        self.bullets.empty()

        # Create new fleet and center ship
        self._create_fleet()
        self.ship.center_ship()

        # Pause for player
        time.sleep(0.5)
    else:
        self.game_active = False

def _check_aliens_bottom(self):
    """Check if any aliens reached bottom."""
    screen_rect = self.screen.get_rect()
    for alien in self.aliens.sprites():
        if alien.rect.bottom >= screen_rect.bottom:
            self._ship_hit()
            break

Center Ship Method

# In Ship class
def center_ship(self):
    """Center ship on screen."""
    self.rect.midbottom = self.screen_rect.midbottom
    self.x = float(self.rect.x)

Game Active Flag

def run_game(self):
    while True:
        self._check_events()

        if self.game_active:  (1)
            self.ship.update()
            self._update_bullets()
            self._update_aliens()

        self._update_screen()
        self.clock.tick(60)
1 Only update game elements when active

Collision Types

Function Use Case

spritecollide(sprite, group, dokill)

One sprite vs group

spritecollideany(sprite, group)

Quick check, returns first hit

groupcollide(group1, group2, dokill1, dokill2)

Group vs group

All use rect overlap detection by default.

Quick Reference

Pattern Code

Create sprite group

aliens = Group()

Add to group

group.add(sprite)

Draw all in group

group.draw(screen)

Update all in group

group.update()

Clear group

group.empty()

Sprite vs group

spritecollideany(sprite, group)

Group vs group

groupcollide(g1, g2, True, True)

Get sprite list

group.sprites()

Count sprites

len(group)

Exercises

13-1. Stars

Create a night sky with randomly positioned stars using a sprite group.

13-2. Better Stars

Use randint() to place stars at random positions across entire screen.

13-3. Raindrops

Create raindrops that fall. When one reaches bottom, remove and create new one at top.

13-4. Steady Rain

Create rows of raindrops that continuously fall and regenerate.

13-5. Sideways Shooter

Create a game where ship is on left, enemies come from right.

Summary

  • Sprites in groups enable batch operations (update, draw)

  • Calculate fleet layout based on sprite and screen dimensions

  • Fleet movement: horizontal until edge, then drop and reverse

  • Collision detection: spritecollideany() and groupcollide()

  • Game state flags control what updates each frame

  • Stats class tracks game data (lives, score)

Next: Scoring and game state management.