๐ 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>
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
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);
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");
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);
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);
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
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
}
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);
}
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);
}
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();
}
}
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);
}
}
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
}
}
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);
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--;
}
}
}
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();
}
}
}
}
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
}
}
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"
});
}
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>
๐ฎ 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)