๐ 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.jsfrom 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
-
bulletCountsdecreases 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
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) CollisioncrashWith() 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 ๐ข EasyTroubleshooting
Problem Solution Player doesn’t move Checkdisplay.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!
๋ต๊ธ ๋จ๊ธฐ๊ธฐ