Skip to content

Space Invaders Example

A complete implementation of the classic Space Invaders game showcasing advanced PixelRoot32 features including sprite animations, collision detection with sweep tests, dynamic music tempo, tilemap backgrounds, and efficient entity management.

Overview

This example demonstrates a fully playable Space Invaders game with: - Player ship with horizontal movement and shooting - Formation of 32 aliens (4 rows × 8 columns) with different types - Defensive bunkers that degrade when hit - Dynamic background music that speeds up as aliens approach - Visual explosion effects - Score and lives system - Win/lose conditions

Architecture

Scene Structure

SpaceInvadersScene (SpaceInvadersScene.h/cpp) - Main game scene managing all entities and game state - Handles game loop, collisions, spawning, and state transitions - Implements custom entity management to work within MAX_ENTITIES limit

Entity Classes

PlayerActor

  • Type: PhysicsActor
  • Features:
  • Horizontal movement controlled by LEFT/RIGHT buttons
  • Shooting with FIRE button
  • Physics-based movement with world boundaries
  • Respawn system after being hit

AlienActor

  • Type: Actor
  • Features:
  • Three types: SQUID (top row, 30 points), CRAB (middle rows, 20 points), OCTOPUS (bottom rows, 10 points)
  • Step-based sprite animations (alternates frames on movement)
  • Formation movement (moves as a group, drops down when hitting edges)
  • Uses MultiSprite for CRAB type (multi-layer sprite)

ProjectileActor

  • Type: PhysicsActor
  • Features:
  • Two types: PLAYER_BULLET (white, moves up) and ENEMY_BULLET (red, moves down)
  • Object pooling (reuses projectiles instead of creating/destroying)
  • Sweep test collision detection for fast-moving projectiles
  • Tracks previous position for accurate collision detection

BunkerActor

  • Type: Actor
  • Features:
  • Health system (starts at 4, decreases when hit)
  • Visual degradation (color changes: Green → Yellow → Red)
  • Collision detection with dynamic hitbox based on remaining health

Background

TilemapBackground - Starfield created using tilemap system - Procedurally generated pattern - Rendered on layer 0 (background)

Key Systems

Collision Detection

The game uses sweep tests for accurate collision detection with fast-moving projectiles:

// Example from SpaceInvadersScene::handleCollisions()
Circle startCircle;
startCircle.x = proj->getPreviousX() + radius;
startCircle.y = proj->getPreviousY() + PROJECTILE_HEIGHT * 0.5f;
startCircle.radius = radius;

Circle endCircle;
endCircle.x = proj->x + radius;
endCircle.y = proj->y + PROJECTILE_HEIGHT * 0.5f;
endCircle.radius = radius;

float tHit = 0.0f;
if (sweepCircleVsRect(startCircle, endCircle, targetBox, tHit)) {
    // Collision detected
}

Why sweep tests? - Projectiles move fast (120 px/s) - Standard AABB can miss collisions between frames - Sweep tests check the path between previous and current position

Entity Management

Due to the MAX_ENTITIES = 32 limit, the scene uses custom entity management:

// Custom vectors instead of Scene's entity system
std::vector<AlienActor*> aliens;
std::vector<ProjectileActor*> projectiles;
std::vector<BunkerActor*> bunkers;

Object Pooling: - Projectiles are pre-allocated (MaxProjectiles = 12) - Projectiles are reused instead of created/destroyed - reset() method reactivates projectiles

Scene Arena (Optional): - Uses PIXELROOT32_ENABLE_SCENE_ARENA if available - Pre-allocates memory for all entities - Avoids heap fragmentation

Dynamic Music Tempo

The background music tempo increases as aliens get closer to the player:

void SpaceInvadersScene::updateMusicTempo() {
    // Find lowest active alien
    float lowestY = /* ... */;

    // Calculate threat factor (0.0 to 1.0)
    float threatFactor = (lowestY - ALIEN_START_Y) * INV_Y_RANGE;

    // Target tempo: 1.0 (slow) to 1.9 (fast)
    float targetTempo = 1.0f + (threatFactor * 0.9f);

    // Smooth interpolation
    currentMusicTempoFactor += (targetTempo - currentMusicTempoFactor) * 0.05f;

    engine.getMusicPlayer().setTempoFactor(currentMusicTempoFactor);
}

Music Tracks: - BGM_SLOW_TRACK: Initial tempo (slow) - BGM_MEDIUM_TRACK: Medium tempo (unused, can be used for transitions) - BGM_FAST_TRACK: Fast tempo (unused, can be used for transitions) - WIN_TRACK: Victory music (plays once) - GAME_OVER_TRACK: Defeat music (plays once)

Visual Effects

Enemy Explosions

  • Simple cross pattern (horizontal + vertical lines)
  • Pool of 8 explosion effects
  • 200ms duration each
  • Reused slots to avoid allocations

Player Explosion

  • 3-frame sprite animation
  • Plays when player is hit
  • Pauses gameplay during animation
  • Respawns player after animation completes

Alien Formation Movement

Aliens move in lockstep formation:

void SpaceInvadersScene::updateAliens(unsigned long deltaTime) {
    stepTimer += scaledDelta;

    if (stepTimer >= stepDelay) {
        // Check if formation hit edge
        bool edgeHit = /* ... */;

        if (edgeHit) {
            moveDirection *= -1; // Reverse direction
            // Drop down
            for (auto* alien : aliens) {
                alien->move(0, ALIEN_DROP_AMOUNT);
            }
        } else {
            // Move horizontally
            float dx = moveDirection * ALIEN_STEP_AMOUNT_X;
            for (auto* alien : aliens) {
                alien->move(dx, 0);
            }
        }
    }
}

Movement Characteristics: - Step-based (not continuous) - Base step delay: 417ms (72 BPM) - Step amount: 2.5 pixels horizontally - Drop amount: 7 pixels when hitting edge - Tempo scales with music tempo factor

Enemy Shooting System

Enemies shoot from bottom-most aliens:

void SpaceInvadersScene::enemyShoot() {
    // Find bottom-most aliens (no alien below them)
    std::vector<AlienActor*> bottomAliens;
    // ... collect bottom aliens ...

    // Difficulty-based chance (increases as aliens die)
    float t = 1.0f - (alive / total);
    int chance = minChance + (t * (maxChance - minChance));

    // Random roll
    if (roll >= chance) {
        // Fire from random bottom alien
    }
}

Shooting Rules: - Only bottom-most aliens can shoot - Maximum 4 enemy bullets active at once - Chance increases as more aliens are destroyed - Difficulty scales from 8% to 30% chance

Code Structure

File Organization

SpaceInvaders/
├── SpaceInvadersScene.h/cpp    # Main scene
├── PlayerActor.h/cpp           # Player ship
├── AlienActor.h/cpp            # Enemy aliens
├── ProjectileActor.h/cpp       # Bullets
├── BunkerActor.h/cpp           # Defensive bunkers
├── GameConstants.h             # Game configuration
└── AlienSprites.h              # Sprite definitions

Game Constants

Key constants defined in GameConstants.h:

// Formation
constexpr int ALIEN_ROWS = 4;
constexpr int ALIEN_COLS = 8;

// Movement
constexpr unsigned long BASE_STEP_DELAY = 417; // 72 BPM
constexpr float ALIEN_STEP_AMOUNT_X = 2.5f;
constexpr float ALIEN_DROP_AMOUNT = 7.0f;

// Projectiles
constexpr int MaxProjectiles = 12;
constexpr float PROJECTILE_SPEED = 120.0f;

// Bunkers
constexpr int BUNKER_COUNT = 4;
constexpr int BUNKER_WIDTH = 24;
constexpr int BUNKER_HEIGHT = 16;

Design Patterns Used

1. Object Pooling

Projectiles are pooled to avoid allocations:

// Pre-allocate in resetGame()
for (int i = 0; i < MaxProjectiles; ++i) {
    ProjectileActor* projectile = new ProjectileActor(0, -PROJECTILE_HEIGHT, ProjectileType::PLAYER_BULLET);
    projectile->deactivate();
    projectiles.push_back(projectile);
}

// Reuse when shooting
for (auto* proj : projectiles) {
    if (!proj->isActive()) {
        proj->reset(px, py, ProjectileType::PLAYER_BULLET);
        break;
    }
}

2. State Machine

Game states: Playing → Paused (on hit) → Game Over

if (gameOver) {
    // Game over state
} else if (isPaused) {
    // Paused during player explosion
    if (!playerExplosion.isActive()) {
        respawnPlayerUnderBunker();
        isPaused = false;
    }
} else {
    // Normal gameplay
}

3. Formation Controller

Aliens are moved as a group by the scene, not individually:

// Scene controls movement
for (auto* alien : aliens) {
    if (alien->isActive()) {
        alien->move(dx, 0); // Scene tells alien to move
    }
}

4. Explosion Pool

Enemy explosions use a fixed pool:

static constexpr int MaxEnemyExplosions = 8;
EnemyExplosion enemyExplosions[MaxEnemyExplosions];

void spawnEnemyExplosion(float x, float y) {
    // Find first available slot
    for (int i = 0; i < MaxEnemyExplosions; ++i) {
        if (!enemyExplosions[i].active) {
            enemyExplosions[i].active = true;
            enemyExplosions[i].x = x;
            enemyExplosions[i].y = y;
            enemyExplosions[i].remainingMs = 200;
            return;
        }
    }
}

Audio Integration

Sound Effects

Player Shoot:

AudioEvent event{};
event.type = WaveType::PULSE;
event.frequency = 880.0f;
event.duration = 0.08f;
event.volume = 0.4f;
event.duty = 0.5f;
engine.getAudioEngine().playEvent(event);

Enemy Hit:

AudioEvent event{};
event.type = WaveType::NOISE;
event.frequency = 600.0f;
event.duration = 0.12f;
event.volume = 0.6f;
engine.getAudioEngine().playEvent(event);

Player Hit:

AudioEvent event{};
event.type = WaveType::NOISE;
event.frequency = 400.0f;
event.duration = 0.18f;
event.volume = 0.7f;
engine.getAudioEngine().playEvent(event);

Background Music

  • Uses MusicPlayer with tempo control
  • Tempo dynamically adjusts based on threat level
  • Different tracks for win/lose conditions

Sprite System

Sprite Formats

  • 1bpp sprites: Player, most aliens, explosions
  • MultiSprite: CRAB alien type (multi-layer sprite)
  • Animations: Step-based (advances on movement, not time)

Sprite Scaling

All sprites use SPRITE_SCALE = 1.25f for larger appearance:

renderer.drawSprite(PLAYER_SHIP_SPRITE,
    static_cast<int>(x),
    static_cast<int>(y),
    SPRITE_SCALE,  // scaleX
    SPRITE_SCALE,  // scaleY
    Color::White);

Performance Optimizations

1. Entity Pooling

  • Projectiles are pooled (12 max)
  • Explosions are pooled (8 max)
  • Avoids allocations during gameplay

2. Scene Arena (Optional)

  • Pre-allocates 8KB buffer for entities
  • All entities allocated from arena
  • Zero heap fragmentation

3. Efficient Collision Detection

  • Sweep tests only for fast projectiles
  • Simple AABB for static/slow objects
  • Early exits in collision loops

4. Custom Entity Management

  • Bypasses Scene's entity limit by using vectors
  • Direct control over entity lifecycle
  • More efficient for this specific use case

Game Flow

Initialization

  1. Create tilemap background
  2. Spawn player at bottom center
  3. Spawn 32 aliens in formation (4×8 grid)
  4. Spawn 4 bunkers evenly spaced
  5. Pre-allocate projectile pool
  6. Start background music

Gameplay Loop

  1. Update Input: Read player movement and shooting
  2. Update Aliens: Move formation, check edges, enemy shooting
  3. Update Projectiles: Move projectiles, check boundaries
  4. Handle Collisions: Sweep tests for projectiles vs aliens/bunkers/player
  5. Update Effects: Explosion animations
  6. Update Music: Adjust tempo based on alien position
  7. Draw: Background, entities, explosions, HUD

Game Over Conditions

  • Win: All aliens destroyed
  • Lose: Player lives reach 0 OR aliens reach player Y position

Respawn System

  1. Player hit → Start explosion animation
  2. Pause gameplay
  3. Wait for explosion to complete
  4. Respawn player under first intact bunker
  5. Resume gameplay

Key Learnings

What This Example Demonstrates

  1. Advanced Collision Detection
  2. Sweep tests for fast-moving objects
  3. Accurate hit detection even at high speeds

  4. Efficient Entity Management

  5. Object pooling for frequently created/destroyed entities
  6. Custom management to work around MAX_ENTITIES limit
  7. Scene Arena for zero-fragmentation allocation

  8. Dynamic Audio

  9. Music tempo synchronization with gameplay
  10. Multiple music tracks for different states
  11. Sound effects for game events

  12. Sprite Animations

  13. Step-based animations (tied to game logic, not time)
  14. MultiSprite for complex sprites
  15. Sprite scaling for visual variety

  16. State Management

  17. Game states (playing, paused, game over)
  18. Smooth transitions between states
  19. Respawn system with visual feedback

  20. Formation Movement

  21. Coordinated group movement
  22. Edge detection and direction reversal
  23. Progressive difficulty (tempo increases)

  24. Visual Effects

  25. Explosion animations
  26. Health-based rendering (bunkers)
  27. HUD with score and lives

Code Examples

Shooting System

// Player shooting
if (fireInputReady && player->wantsToShoot()) {
    // Check if player bullet already active
    bool hasPlayerBullet = false;
    for (auto* proj : projectiles) {
        if (proj->isActive() && proj->getType() == ProjectileType::PLAYER_BULLET) {
            hasPlayerBullet = true;
            break;
        }
    }

    if (!hasPlayerBullet) {
        // Find inactive projectile and reuse it
        for (auto* proj : projectiles) {
            if (!proj->isActive()) {
                float px = player->x + (PLAYER_WIDTH - PROJECTILE_WIDTH) / 2.0f;
                float py = player->y - PROJECTILE_HEIGHT;
                proj->reset(px, py, ProjectileType::PLAYER_BULLET);

                // Play shoot sound
                AudioEvent event{};
                event.type = WaveType::PULSE;
                event.frequency = 880.0f;
                event.duration = 0.08f;
                event.volume = 0.4f;
                engine.getAudioEngine().playEvent(event);
                break;
            }
        }
    }
}

Collision Detection with Sweep Test

// Check projectile vs alien collision
Circle startCircle;
startCircle.x = proj->getPreviousX() + radius;
startCircle.y = proj->getPreviousY() + PROJECTILE_HEIGHT * 0.5f;
startCircle.radius = radius;

Circle endCircle;
endCircle.x = proj->x + radius;
endCircle.y = proj->y + PROJECTILE_HEIGHT * 0.5f;
endCircle.radius = radius;

float tHit = 0.0f;
pixelroot32::core::Rect targetBox = alien->getHitBox();

if (sweepCircleVsRect(startCircle, endCircle, targetBox, tHit) ||
    proj->getHitBox().intersects(targetBox)) {
    // Hit! Deactivate projectile and kill alien
    proj->deactivate();
    alien->kill();
    score += alien->getScoreValue();
    spawnEnemyExplosion(alien->x + alien->width * 0.5f, 
                        alien->y + alien->height * 0.5f);
}

Running the Example

Prerequisites

  • PixelRoot32 Game Engine installed
  • ESP32 or Native build configured
  • Display and input configured

Build and Run

  1. Include the example in your project:

    #include "examples/SpaceInvaders/SpaceInvadersScene.h"
    

  2. In your main.cpp:

    spaceinvaders::SpaceInvadersScene gameScene;
    
    void setup() {
        engine.init();
        gameScene.init();
        engine.setScene(&gameScene);
    }
    

  3. Build and upload (ESP32) or run (Native)

Controls

  • LEFT/RIGHT: Move player ship
  • FIRE: Shoot projectile
  • FIRE (on game over): Restart game

See Also