DEV Community

Cover image for ๐Ÿš€ Build Your First Space Shooter Game with Limn Engine
Kehinde Owolabi
Kehinde Owolabi

Posted on

๐Ÿš€ Build Your First Space Shooter Game with Limn Engine

๐Ÿš€ Build Your First Space Shooter Game with Limn Engine

A Complete Step-by-Step Tutorial for JavaScript Beginners

Welcome! In this tutorial, you'll build a complete space shooter game using Limn Engine โ€” a zeroโ€‘configuration 2D game engine that runs in your browser.

What you'll build: A spaceship that moves, shoots bullets, fights waves of enemies, and keeps score. All in about 100 lines of code.

By the end, you'll understand:

  • How to create a game loop
  • How to handle keyboard input
  • How to detect collisions
  • How to use particles for visual effects
  • How to manage game state (lives, score, game over)

๐ŸŽฎ Want to play the finished game? Click here to play Space Shooter Live!


Before We Start

What You Need

  • A text editor (VS Code, Notepad, or any code editor)
  • A web browser (Chrome, Firefox, Edge)
  • Limn Engine โ€” download epic.js from limn-engine-doc.vercel.app

What You Should Know

  • Basic JavaScript (variables, functions, arrays, if-statements)
  • How to open an HTML file in a browser

No game development experience required!


Step 1: The HTML Structure

Every Limn Engine game starts with a simple HTML file.

<!doctype html>
<html>
  <head>
    <script src="asset/epic.js"></script>
  </head>
  <body>
  <script>
    // All your game code goes here
  </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • <script src="asset/epic.js"> โ€” loads the Limn Engine library
  • Everything inside the second <script> tag is your game code

Save this as game.html and open it in your browser. You should see a blank canvas with a blue gradient background.


Step 2: Setting Up the Game

The first thing we need is a Display โ€” this is the engine that creates the canvas, runs the game loop, and handles input.

const display = new Display();
display.perform();        // Activates performance mode (dual-canvas rendering)
display.start(800, 600);  // Creates an 800ร—600 canvas
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • new Display() โ€” creates the engine
  • display.perform() โ€” turns on high-performance mode
  • display.start(800, 600) โ€” creates a canvas 800 pixels wide and 600 pixels tall

Add this code now. If you refresh your browser, you'll see a black canvas.


Step 3: Creating the Player

Every visible object in Limn Engine is a Component โ€” that includes the player, enemies, bullets, and even text.

const player = new Component(40, 40, "cyan", 400, 520, "rect");
display.add(player);
Enter fullscreen mode Exit fullscreen mode

Breakdown:
| Parameter | Value | Meaning |
|-----------|-------|---------|
| 40, 40 | width, height | The player is a 40ร—40 pixel square |
| "cyan" | color | The player is cyan (light blue) |
| 400, 520 | x, y | Position at 400px from left, 520px from top |
| "rect" | type | It's a rectangle (shape) |

Add this code. Now when you refresh, you'll see a cyan square near the bottom of the screen.


Step 4: Adding a Background

Let's make the background look like space.

display.lgradient("top", "royalblue", "darkblue");
Enter fullscreen mode Exit fullscreen mode

Breakdown:

  • lgradient โ€” linear gradient (colors blend from one to another)
  • "top" โ€” the gradient goes from top to bottom
  • "royalblue" โ€” top color (light blue)
  • "darkblue" โ€” bottom color (dark blue)

Add this code below display.start(). Your game now has a space sky background.


Step 5: Adding UI โ€” Score and Lives

We need to display the score and remaining lives. For text, we use Tctxt โ€” a special component for text.

const scoreText = new Tctxt("32px", "Arial", "white", 20, 50);
scoreText.setText("Score: 0");
display.add(scoreText);

const livesText = new Tctxt("24px", "Arial", "red", 20, 100);
livesText.setText("Lives: 3: "+" Bullets: 12");
display.add(livesText);
Enter fullscreen mode Exit fullscreen mode

Breakdown:
| Parameter | scoreText | livesText |
|-----------|-----------|-----------|
| "32px" / "24px" | Font size | Smaller for lives |
| "Arial" | Font family | Same font |
| "white" / "red" | Text color | Score is white, lives are red |
| 20, 50 | Position | Score at top-left |
| 20, 100 | Position | Lives below score |

Add this code. Now you have a score and lives display.


Step 6: Adding Particles

We need particles for visual effects like explosions and engine exhaust.

const particles = new ParticleSystem(display);
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • new ParticleSystem(display) โ€” creates a particle system linked to our display
  • We'll use this later for explosions and the engine trail

Add this code after creating the display.


Step 7: Game Variables

We need variables to track the game state:

let enemies = [];          // Array to hold all enemies
let bullets = [];          // Array to hold all bullets
let score = 0;             // Player's score
let lives = 3;             // Player's lives
let enemyTimer = 0;        // Timer for spawning enemies
let invincibleFrames = 0;  // Frames of invincibility after being hit
let bulletCounts = 12;     // How many bullets the player has
Enter fullscreen mode Exit fullscreen mode

Add this code after your UI setup.


Step 8: Creating the Update Function

The update function is called 60 times per second โ€” it's where all the game logic happens.

function update(dt) {
    // All game logic goes here
}
Enter fullscreen mode Exit fullscreen mode

What is dt? dt stands for "delta time" โ€” the time since the last frame. It's very small (around 0.016 seconds at 60 FPS). We multiply speeds by dt so the game runs at the same speed on all devices.

Add this empty function after your game variables.


Step 9: Making the Player Move

Inside the update function, let's handle player movement:

function update(dt) {
    // Player movement
    if (display.keys[37]) {         // Left arrow key
        player.speedX = -400 * dt;
    } else if (display.keys[39]) {  // Right arrow key
        player.speedX = 400 * dt;
    } else {
        player.speedX = 0;
    }

    // Keep player on screen
    move.bound(player);
}
Enter fullscreen mode Exit fullscreen mode

Key codes:

  • 37 โ€” Left arrow
  • 39 โ€” Right arrow

What's happening:

  • If the left arrow is pressed, the player moves left at 400 pixels per second
  • If the right arrow is pressed, the player moves right at 400 pixels per second
  • If neither is pressed, the player stops
  • move.bound(player) โ€” prevents the player from going off-screen

Test your game: You can now move the player left and right!


Step 10: Creating Enemies

We need enemies that come from the top of the screen:

function createEnemy() {
    const e = new Component(35, 35, "red", Math.random() * 750, -35, "rect");
    e.speedY = 1.50;
    display.add(e);
    enemies.push(e);
}
Enter fullscreen mode Exit fullscreen mode

Breakdown:

  • Math.random() * 750 โ€” random X position across the screen
  • -35 โ€” starts above the screen
  • e.speedY = 1.50 โ€” moves downward at 1.5 pixels per frame

Now add this to the update function:

function update(dt) {
    // ... existing code ...

    // Spawn enemies
    enemyTimer++;
    if (enemyTimer > 45) {
        enemyTimer = 0;
        createEnemy();
    }
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • Every 45 frames (about 0.75 seconds), a new enemy appears
  • Enemies spawn at a random X position above the screen
  • They move downward

Step 11: Shooting Bullets

We need a function to create bullets:

function shoot() {
    if (bulletCounts > 0) {
        // Create bullet at player's position
        const b = new Component(5, 10, "yellow", player.x + 17.5, player.y, "rect");
        b.speedY = -5.00;
        display.add(b);
        bullets.push(b);
        bulletCounts--;
        livesText.setText("Lives: " + lives + ", Bullet: " + bulletCounts);
    }
}
Enter fullscreen mode Exit fullscreen mode

Breakdown:

  • player.x + 17.5 โ€” centers the bullet on the player (half of player width)
  • b.speedY = -5.00 โ€” bullet moves upward
  • bulletCounts-- โ€” uses one bullet

Now add this to the update function:

function update(dt) {
    // ... existing code ...

    // Shoot with SPACE
    if (display.keys[32]) {
        shoot();
        display.keys[32] = false;  // Prevent holding down SPACE
    }
}
Enter fullscreen mode Exit fullscreen mode

Key code: 32 = Spacebar

What's happening:

  • When SPACE is pressed, we call shoot()
  • A bullet appears at the player's position
  • The bullet travels upward
  • bulletCounts decreases and the UI updates
  • display.keys[32] = false โ€” ensures one bullet per press

Step 12: Refilling Bullets

We want bullets to refill every 7 seconds:

setInterval(() => {
    bulletCounts = 12;
}, 7000);
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • Every 7 seconds (7000 milliseconds), bullets reset to 12

Step 13: Updating Bullets

Bullets need to move upward and be removed when off-screen:

function update(dt) {
    // ... existing code ...

    // Update bullets
    for (let i = 0; i < bullets.length; i++) {
        bullets[i].y += bullets[i].speedY * dt;
        if (bullets[i].y < -50) {
            bullets[i].hide();        // Hide from view
            bullets.splice(i, 1);     // Remove from array
            i--;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • Each bullet moves upward at its speed
  • If a bullet goes above the screen (y < -50), it's hidden and removed from the array

Step 14: Enemy Logic and Collisions

This is the most important part โ€” handling enemy movement and collisions:

function update(dt) {
    // ... existing code ...

    // Update enemies
    for (let i = 0; i < enemies.length; i++) {
        const e = enemies[i];
        e.y += e.speedY * dt;

        // Enemy vs player collision
        if (e.crashWith(player) && invincibleFrames === 0) {
            lives--;
            livesText.setText("Lives: " + lives + ", Bullet: " + bulletCounts);
            invincibleFrames = 60;
            display.camera.shake(8, 8);
            e.hide();
            enemies.splice(i, 1);
            i--;

            if (lives <= 0) {
                display.stop();
                alert("Game Over! Score: " + score);
                location.reload();
            }
            continue;
        }

        // Enemy vs bullet collision
        for (let j = 0; j < bullets.length; j++) {
            if (e.crashWith(bullets[j])) {
                move.particles.explosion(particles, e.x, e.y, 20);
                score += 10;
                scoreText.setText("Score: " + score);
                display.camera.shake(3, 3);
                e.hide();
                bullets[j].hide();
                enemies.splice(i, 1);
                bullets.splice(j, 1);
                i--;
                break;
            }
        }

        // Enemy escapes bottom
        if (enemies[i] && enemies[i].y > 650) {
            enemies[i].hide();
            score -= 12;
            scoreText.setText("Score: " + score);
            enemies.splice(i, 1);
            i--;

            if (score < 0) {
                display.stop();
                alert("Game Over! Score: " + score);
                location.reload();
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • Enemies move down the screen
  • If enemy hits player: Lose a life, become invincible, screen shakes
  • If bullet hits enemy: Explosion, +10 score, screen shakes
  • If enemy escapes: -12 score, game over if score goes negative

Step 15: Invincibility and Camera Follow

Add invincibility flashing and camera follow:

function update(dt) {
    // Update particles
    particles.update();

    // Camera follows player
    display.camera.follow(player);

    // Invincibility blinking
    if (invincibleFrames > 0) {
        invincibleFrames--;
        player.alpha = 0.5;  // Semi-transparent
    } else {
        player.alpha = 1;    // Fully visible
    }
}
Enter fullscreen mode Exit fullscreen mode

Note: In Limn Engine, alpha controls transparency. 1 is fully visible, 0.5 is half-transparent.


Step 16: Engine Exhaust Particles

Add a trail of particles behind the player:

function update(dt) {
    // ... existing code ...

    // Engine exhaust particles
    particles.emit(player.x + 20, player.y + 40, {
        color: "orange",
        life: 15,
        alphaFade: 0.07,
        speedY: 1,
        width: 5,
        height: 5,
        type: "circle"
    });
}
Enter fullscreen mode Exit fullscreen mode

Particle options explained:
| Option | Value | Meaning |
|--------|-------|---------|
| color | "orange" | The particle color |
| life | 15 | Lives for 15 frames |
| alphaFade | 0.07 | Fades out gradually |
| speedY | 1 | Drifts downward slightly |
| width/height | 5 | Particle size |
| type | "circle" | Round particles |


Step 17: Putting It All Together

Here's the complete code โ€” the exact game you can play at test9.html:

<!doctype html>
<html>
  <head>
    <script src="asset/epic.js"></script>
  </head>
  <body>
  <script>
    const display = new Display();
display.perform();
display.start(800, 600);
const particles = new ParticleSystem(display);

// Player
const player = new Component(40, 40, "cyan", 400, 520, "rect");
display.add(player);
display.lgradient("top", "royalblue", "darkblue")
// UI
const scoreText = new Tctxt("32px", "Arial", "white", 20, 50);
scoreText.setText("Score: 0");
display.add(scoreText);

const livesText = new Tctxt("24px", "Arial", "red", 20, 100);
livesText.setText("Lives: 3: "+" Bullets: 12");
display.add(livesText);

// Game state
let enemies = [], bullets = [], score = 0, lives = 3, enemyTimer = 0, invincibleFrames = 0;

function createEnemy() {
    const e = new Component(35, 35, "red", Math.random() * 750, -35, "rect");
    e.speedY = 1.50;
    display.add(e);
    enemies.push(e);
}
let bulletCounts = 12
function shoot() {
  if(bulletCounts >0){
    const b = new Component(5, 10, "yellow", player.x + 17.5, player.y, "rect");
    b.speedY = -5.00;
    display.add(b);
    bullets.push(b);
    bulletCounts -= 1
    livesText.setText("Lives: " + lives+", Bullet: "+  bulletCounts);
  }
}
setInterval(()=>{
  bulletCounts = 12
},7000)
function update(dt) {
    particles.update();
display.camera.follow(player);

scoreText.x = 20+display.camera.x
scoreText.y = 50+display.camera.y
  livesText.x = 20+display.camera.x
livesText.y = 100+display.camera.y
    // Invincibility blinking
    if (invincibleFrames > 0) {
        invincibleFrames--;
        player.alpha = 0.5;
    } else player.alpha = 1;

    // Player movement
    if (display.keys[37]) player.speedX = -400 * dt;
    else if (display.keys[39]) player.speedX = 400 * dt;
    else player.speedX = 0;
    move.bound(player);

    // Shoot with SPACE
    if (display.keys[32]) { shoot(); display.keys[32] = false; }

    // Spawn enemies
    if (++enemyTimer > 45) { enemyTimer = 0; createEnemy(); }

    // Bullet update
    for (let i = 0; i < bullets.length; i++) {
        bullets[i].y += bullets[i].speedY * dt;
        if (bullets[i].y < -50) { bullets[i].hide(); bullets.splice(i,1); i--; }
    }

    // Enemy logic and collisions
    for (let i = 0; i < enemies.length; i++) {
        const e = enemies[i];
        e.y += e.speedY * dt;

        // Enemy vs player
        if (e.crashWith(player) && invincibleFrames === 0) {
            lives--;
            livesText.setText("Lives: " + lives+", Bullet: "+  bulletCounts);
            invincibleFrames = 60;
            display.camera.shake(8, 8);
            e.hide(); enemies.splice(i,1); i--;
            if (lives <= 0) { display.stop(); alert("Game Over! Score: " + score); location.reload(); }
            continue;
        }

        // Enemy vs bullet
        for (let j = 0; j < bullets.length; j++) {
            if (e.crashWith(bullets[j])) {
                move.particles.explosion(particles, e.x, e.y, 20);
                score += 10;
                scoreText.setText("Score: " + score);
                display.camera.shake(3, 3);
                e.hide(); bullets[j].hide();
                enemies.splice(i,1); bullets.splice(j,1);
                i--; break;
            }
        }

        if (enemies[i] && enemies[i].y > 650) { enemies[i].hide();score-=12;scoreText.setText("Score: " + score); enemies.splice(i,1); i--;
                                              if(score<0){
                                                display.stop(); alert("Game Over! Score: " + score); location.reload();
                                              }
                                              }
    }

    // Engine exhaust particles
    particles.emit(player.x + 20, player.y + 40, { color: "orange", life: 15, alphaFade: 0.07, speedY: 1, width: 5, height: 5, type: "circle" });
}
  </script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฎ Play the Finished Game

You can play the complete game right now:

Click here to play Space Shooter Live!


Summary: What You Learned

Concept How It Works
Display Creates the canvas and game loop
Component Every visible object (player, enemies, bullets)
Tctxt Text display (score, lives)
ParticleSystem Visual effects (explosions, exhaust)
Collision crashWith() detects when objects touch
Keyboard input display.keys[keyCode]
Delta time dt makes movement frameโ€‘rate independent
Game state Arrays, variables, and logic
hide() Hides objects without destroying them
setInterval Timer for bullet refills

Next Steps

What to Try Difficulty
Add a boss enemy ๐ŸŸก Medium
Add powerโ€‘up pickups ๐ŸŸก Medium
Add sound effects ๐ŸŸข Easy
Add a start menu ๐ŸŸข Easy
Add a high score system ๐ŸŸก Medium
Use images instead of shapes ๐ŸŸข Easy

Troubleshooting

Problem Solution
Player doesn't move Check display.keys[37] and [39]
Bullets don't appear Check shoot() is called and bulletCounts > 0
Enemies don't spawn Check enemyTimer > 45 is reached
Game crashes Check for hide() on already hidden objects
Particles don't show Call particles.update() in update()
Player doesn't blink Check player.alpha is being set correctly

Congratulations! You've built a complete space shooter game with Limn Engine. ๐Ÿš€

Now go customize it โ€” add different enemy types, power-ups, or your own graphics!

Top comments (0)