๐ 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.jsfrom 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>
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
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
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);
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;
Now add this to the update function:
function update(dt) {
// Ball movement
ball.x += ball.speedX * dt;
ball.y += ball.speedY * dt;
}
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
}
}
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;
}
}
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
returnprevents 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;
}
}
}
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
}
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;
}
}
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
}
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);
}
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
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;
}
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>
๐ฎ 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)