DEV Community

Cover image for ๐Ÿ“ Build a Ball Game with Limn Engine
Kehinde Owolabi
Kehinde Owolabi

Posted on

๐Ÿ“ Build a Ball Game with Limn Engine

๐Ÿ“ Build a Ball Game with Limn Engine

A Complete Step-by-Step Tutorial for JavaScript Beginners

Welcome! In this tutorial, you'll build a complete ball-and-paddle game (like Breakout) using Limn Engine โ€” a zeroโ€‘configuration 2D game engine that runs in your browser.

What you'll build: A paddle that moves left and right, a bouncing ball that speeds up over time, score tracking, high scores saved in your browser, and a game-over screen. All in about 100 lines of code.

By the end, you'll understand:

  • How to create game objects (paddle, ball, floor)
  • How to handle keyboard and touch input
  • How to detect and respond to collisions
  • How to track and save high scores
  • How to manage game states (playing, game over)

๐ŸŽฎ Want to play the finished game? Click here to play Ball Game 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, 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 lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Ball game</title>
  <script src="asset/epic.js"></script>
  <style>
    canvas{
      border: solid red 5px;
    }
  </style>
</head>
<body bgcolor="darkblue">
  <dialog open style="background:linear-gradient(darkblue,royalblue);"></dialog>
  <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
  • <style> โ€” adds a red border around the canvas
  • <dialog> โ€” acts as a container for the canvas with a gradient background
  • Everything inside the second <script> tag is your game code

Save this as ballgame.html and open it in your browser.


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, document.querySelector("dialog"));  // Creates an 800ร—600 canvas inside the dialog
display.rgradient("royalblue", "darkblue");  // Radial gradient background
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, document.querySelector("dialog")) โ€” creates a canvas 800 pixels wide and 600 pixels tall inside the <dialog> element
  • display.rgradient() โ€” creates a radial gradient background from royal blue to dark blue

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


Step 3: Creating the Paddle and Ball

Every visible object in Limn Engine is a Component โ€” that includes the paddle, ball, floor, and even text.

const player = new Component(100, 30, "magenta", 0, 0);  // Paddle
const ball = new Component(12, 12, "aqua", 10, 10);     // Ball
const floor = new Component(800, 10, "black", 0, 590);  // Floor (game over trigger)

move.position(player, "bottom", 10);  // Position paddle at bottom center
Enter fullscreen mode Exit fullscreen mode

Breakdown:
| Object | Width | Height | Color | Position | Purpose |
|--------|-------|--------|-------|----------|---------|
| player | 100 | 30 | magenta | Bottom center | Paddle controlled by player |
| ball | 12 | 12 | aqua | (10, 10) | Bouncing ball |
| floor | 800 | 10 | black | (0, 590) | Triggers game over on contact |

Add this code. You'll see a magenta paddle at the bottom and a small aqua ball at the top-left.


Step 4: Adding the Game Objects to the Display

To make objects appear on screen, you must add them to the display:

display.add(floor);
display.add(player);
display.add(ball);
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • display.add() โ€” registers each component with the engine
  • Objects are drawn in the order they're added (floor first, ball on top)

Add this code. Now all three objects are visible.


Step 5: Ball Movement

We need the ball to move and bounce off walls. First, set its initial speed:

let ballSpeedX = 300;  // Horizontal speed (pixels per second)
let ballSpeedY = 500;  // Vertical speed (pixels per second)
ball.speedX = 300;
ball.speedY = 500;
Enter fullscreen mode Exit fullscreen mode

Now add this to the update function:

function update(dt) {
    // Ball movement
    ball.x += ball.speedX * dt;
    ball.y += ball.speedY * dt;
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • dt (delta time) ensures the ball moves at the same speed on all devices
  • The ball moves 300 pixels right and 500 pixels down per second

Test your game: The ball will start moving diagonally.


Step 6: Ball vs Walls

The ball needs to bounce off the walls:

function update(dt) {
    // ... ball movement code ...

    // Ball vs walls
    if(ball.x <= 0) {
        ball.x = 0;
        ball.speedX = Math.abs(ball.speedX);  // Bounce right
    }
    if(ball.x + ball.width >= 800) {
        ball.x = 800 - ball.width;
        ball.speedX = -Math.abs(ball.speedX);  // Bounce left
    }
    if(ball.y <= 0) {
        ball.y = 0;
        ball.speedY = Math.abs(ball.speedY);  // Bounce down
    }
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • Math.abs() โ€” makes the speed positive or negative to reverse direction
  • ball.width โ€” accounts for the ball's size when hitting the right wall

Test your game: The ball now bounces off the left, right, and top walls.


Step 7: Game Over โ€” Ball vs Floor

If the ball hits the floor, it's game over:

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

    // Ball vs floor (GAME OVER)
    if(ball.y + ball.height >= 590) {
        gameOver = true;
        display.scene = 1;  // Switch to scene 1 (game over)
        loseTxt.setText("GAME OVER!\nScore: " + score + "\nHigh Score: " + highScore + "\nPress SPACE to restart");
        return;
    }
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • ball.y + ball.height โ€” checks if the ball's bottom edge reaches the floor
  • gameOver = true โ€” stops the game logic
  • display.scene = 1 โ€” switches to scene 1 (game over screen)
  • The return prevents further code from running this frame

Step 8: Ball vs Paddle (Scoring)

When the ball hits the paddle, it bounces back and you score a point:

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

    // Ball vs paddle
    if(ball.crashWith(player) && ball.speedY > 0) {
        ball.y = player.y - ball.height;
        ball.speedY = -Math.abs(ball.speedY);  // Bounce upward
        score++;
        scoreTxt.setText("Score: " + score);
        display.camera.shake(3, 3);  // Small screen shake for feedback

        // Increase ball speed gradually (gets harder!)
        ballSpeedX += 10;
        ballSpeedY += 10;
        ball.speedX = (ball.speedX > 0 ? 1 : -1) * ballSpeedX;
        ball.speedY = (ball.speedY > 0 ? 1 : -1) * ballSpeedY;

        // Update high score
        if(score > highScore) {
            highScore = score;
            localStorage.hs = highScore;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • crashWith() โ€” detects collision between the ball and paddle
  • ball.speedY > 0 โ€” ensures the ball is moving downward (prevents paddle from "sucking" the ball)
  • ball.y = player.y - ball.height โ€” places the ball on top of the paddle
  • display.camera.shake(3, 3) โ€” adds a small screen shake for feedback
  • The ball speeds up gradually with each hit

Step 9: Player Movement (Keyboard)

The player controls the paddle with the arrow keys or WASD:

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

    // Player movement (keyboard)
    if(display.keys[37] || display.keys[65]) {  // Left arrow or A
        player.x -= 700 * dt;
    } else if(display.keys[39] || display.keys[68]) {  // Right arrow or D
        player.x += 700 * dt;
    }
    move.bound(player);  // Keep paddle on screen
}
Enter fullscreen mode Exit fullscreen mode

Key codes:

  • 37 โ€” Left arrow
  • 39 โ€” Right arrow
  • 65 โ€” A key
  • 68 โ€” D key

What's happening:

  • The paddle moves at 700 pixels per second
  • move.bound(player) โ€” keeps the paddle on screen

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


Step 10: Player Movement (Touch Controls)

We also need touch controls for mobile devices:

// Create touch buttons
let leftbtn = new Component(100, 100, "rgba(0,0,0,0)", 50, 500);
let rightbtn = new Component(100, 100, "rgba(0,0,0,0)", 600, 500);
display.add(leftbtn);
display.add(rightbtn);

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

    // Touch controls
    if(leftbtn.clicked() && display.x) {
        player.x -= 700 * dt;
    }
    if(rightbtn.clicked() && display.x) {
        player.x += 700 * dt;
    }
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • Invisible touch buttons on the left and right sides of the screen
  • clicked() โ€” detects if the button was tapped
  • Works on both desktop and mobile

Step 11: Restarting the Game

When the game is over, pressing SPACE restarts it:

let gameOver = false;

function update(dt) {
    // Restart
    if(gameOver && display.keys[32]) {
        gameOver = false;
        display.scene = 0;
        score = 0;
        ball.x = 10;
        ball.y = 10;
        ball.speedX = 300;
        ball.speedY = 500;
        ballSpeedX = 300;
        ballSpeedY = 500;
        move.position(player, "bottom", 10);
        display.keys[32] = false;  // Prevent holding SPACE
    }

    if(gameOver) return;  // Skip game logic if game over
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • display.keys[32] โ€” checks if SPACE is pressed
  • gameOver โ€” prevents game logic from running while on game over screen
  • display.keys[32] = false โ€” ensures one press = one restart
  • All game state resets to starting values

Step 12: UI โ€” Score and High Score

We need text to display the score and high score:

const scoreTxt = new Tctxt("30px", "Monospace", "white", 25, 25);
display.add(scoreTxt);

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

    // Score display
    scoreTxt.setText("Score: " + score + " | High: " + highScore);
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • Tctxt โ€” creates a text component
  • The score updates every frame
  • High score is retrieved from localStorage

Step 13: Game Over Screen

When the game ends, show a game over message:

const loseTxt = new Tctxt("28px", "bold impact", "red", 50, 270);
loseTxt.setText("GAME OVER!\nPress SPACE to restart");
display.add(loseTxt, 1);  // Scene 1 = game over
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • The game over text is added to scene 1
  • It's hidden during gameplay (scene 0)
  • When display.scene = 1, it becomes visible

Step 14: High Score with localStorage

We save the high score in the browser's localStorage so it persists between sessions:

// Load high score
if(!localStorage.hs) {
    localStorage.hs = 0;
}
let highScore = Number(localStorage.hs);

// Update high score
if(score > highScore) {
    highScore = score;
    localStorage.hs = highScore;
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • localStorage โ€” stores data in the browser
  • The high score is saved when the player beats it
  • It's loaded when the page loads

Step 15: Putting It All Together

Here's the complete code โ€” the exact game you can play at https://limn-engine-doc.vercel.app/ballgame.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Ball game</title>
  <script src="asset/epic.js"></script>
  <style>
    canvas{
      border: solid red 5px;
    }
  </style>
</head>
<body bgcolor="darkblue">
  <dialog open style="background:linear-gradient(darkblue,royalblue);"></dialog>
  <script>
    // ===== SETUP =====
    const display = new Display();
    display.perform();
    display.start(800, 600, document.querySelector("dialog"));
    display.rgradient("royalblue", "darkblue");

    // ===== GAME OBJECTS =====
    const player = new Component(100, 30, "magenta", 0, 0);
    const ball = new Component(12, 12, "aqua", 10, 10);
    const floor = new Component(800, 10, "black", 0, 590);

    move.position(player, "bottom", 10);

    // Ball velocity (pixels per second)
    let ballSpeedX = 300;
    let ballSpeedY = 500;
    ball.speedX = 300;
    ball.speedY = 500;

    // ===== HIGH SCORE =====
    if(!localStorage.hs) localStorage.hs = 0;
    let highScore = Number(localStorage.hs);

    // ===== UI =====
    const scoreTxt = new Tctxt("30px", "Monospace", "white", 25, 25);
    const loseTxt = new Tctxt("28px", "bold impact", "red", 50, 270);
    loseTxt.setText("GAME OVER!\nPress SPACE to restart");
    display.add(scoreTxt);
    display.add(loseTxt, 1);  // Scene 1 = game over

    // ===== TOUCH CONTROLS =====
    let leftbtn = new Component(100, 100, "rgba(0,0,0,0)", 50, 500);
    let rightbtn = new Component(100, 100, "rgba(0,0,0,0)", 600, 500);
    display.add(leftbtn);
    display.add(rightbtn);

    let score = 0;
    let gameOver = false;

    // ===== ADD EVERYTHING =====
    display.add(floor);
    display.add(player);
    display.add(ball);

    // ===== GAME LOOP =====
    function update(dt) {
      // --- Restart ---
      if(gameOver && display.keys[32]) {
        gameOver = false;
        display.scene = 0;
        score = 0;
        ball.x = 10;
        ball.y = 10;
        ball.speedX = 300;
        ball.speedY = 500;
        ballSpeedX = 300;
        ballSpeedY = 500;
        move.position(player, "bottom", 10);
        display.keys[32] = false;
      }

      if(gameOver) return;

      // --- Player movement (keyboard) ---
      if(display.keys[37] || display.keys[65]) {
        player.x -= 700 * dt;
      } else if(display.keys[39] || display.keys[68]) {
        player.x += 700 * dt;
      }

      // --- Touch controls ---
      if(leftbtn.clicked() && display.x) {
        player.x -= 700 * dt;
      }
      if(rightbtn.clicked() && display.x) {
        player.x += 700 * dt;
      }

      move.bound(player);

      // --- Ball movement ---
      ball.x += ball.speedX * dt;
      ball.y += ball.speedY * dt;

      // --- Ball vs walls ---
      if(ball.x <= 0) {
        ball.x = 0;
        ball.speedX = Math.abs(ball.speedX);
      }
      if(ball.x + ball.width >= 800) {
        ball.x = 800 - ball.width;
        ball.speedX = -Math.abs(ball.speedX);
      }
      if(ball.y <= 0) {
        ball.y = 0;
        ball.speedY = Math.abs(ball.speedY);
      }

      // --- Ball vs floor (GAME OVER) ---
      if(ball.y + ball.height >= 590) {
        gameOver = true;
        display.scene = 1;
        loseTxt.setText("GAME OVER!\nScore: " + score + "\nHigh Score: " + highScore + "\nPress SPACE to restart");
        return;
      }

      // --- Ball vs paddle ---
      if(ball.crashWith(player) && ball.speedY > 0) {
        ball.y = player.y - ball.height;
        ball.speedY = -Math.abs(ball.speedY);
        score++;
        scoreTxt.setText("Score: " + score);
        display.camera.shake(3, 3);

        // Increase ball speed gradually
        ballSpeedX += 10;
        ballSpeedY += 10;
        ball.speedX = (ball.speedX > 0 ? 1 : -1) * ballSpeedX;
        ball.speedY = (ball.speedY > 0 ? 1 : -1) * ballSpeedY;

        // Update high score
        if(score > highScore) {
          highScore = score;
          localStorage.hs = highScore;
        }
      }

      // --- Score display ---
      scoreTxt.setText("Score: " + score + " | High: " + highScore);
    }
  </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 Ball Game Live!


Summary: What You Learned

Concept How It Works
Display Creates the canvas and game loop
Component Every visible object (paddle, ball, floor)
Tctxt Text display (score, game over)
Collision crashWith() detects when objects touch
Keyboard input display.keys[keyCode]
Touch input clicked() for touch buttons
Delta time dt makes movement frameโ€‘rate independent
Game state gameOver boolean + display.scene
localStorage Saves high score in browser
Scene management display.scene = 1 for game over

Next Steps

What to Try Difficulty
Add sound effects ๐ŸŸข Easy
Add a "Start" screen ๐ŸŸข Easy
Add power-ups (wider paddle, slower ball) ๐ŸŸก Medium
Add bricks to break (like Breakout) ๐ŸŸก Medium
Add multiple lives ๐ŸŸก Medium

Troubleshooting

Problem Solution
Paddle doesn't move Check display.keys[37] and [39]
Ball doesn't bounce Check collision conditions and speed values
Game over doesn't trigger Check ball.y + ball.height >= 590
High score doesn't save Check localStorage is supported in your browser
Touch controls don't work Make sure buttons are added to display and clicked() is called

Congratulations! You've built a complete ball-and-paddle game with Limn Engine. ๐Ÿ“

Now go customize it โ€” add more features, different colors, or your own creative twist!

Top comments (0)