Case Study: Building an Arcade Space Shooter (test9.html)
In the test9.html implementation, Limn Engine's rendering features are used to build a classic top-down space shooter. This project showcases particle systems, screen shakes, shooting loops, and canvas gradients.
1. Canvas Setup with Linear Gradients
Instead of a flat background color, the workspace is stylized using a royal-blue-to-dark-blue sky backdrop created directly through the engine context:
const display = new Display();
display.perform(); // Activate internal canvas caching
display.start(800, 600);
// Fills the background with an elegant linear color trend
display.lgradient("top", "royalblue", "darkblue");
2. Setting up the Particle System and Engine Exhausts
To make the spaceship feel alive, we can hook up a ParticleSystem object. By emitting particles at the player's base during each frame update, we simulate moving rocket thrusters:
const particles = new ParticleSystem(display);
function update() {
particles.update(); // Keep calculations rendering continuously
// Create a streaming rocket exhaust stream trailing behind the spaceship
particles.emit(player.x + 20, player.y + 40, {
color: "orange",
life: 15,
size: 4,
speedX: (Math.random() - 0.5) * 2,
speedY: 2
});
}
3. Automated Enemy Spawning and Array Loops
Enemies are pushed into a tracking array map layout. As they slide down the layout grid via speedY, we inspect their coordinate limits and apply structural score penalties if they escape the bottom edge:
let enemies = [];
function createEnemy() {
const e = new Component(35, 35, "red", Math.random() * 750, -35, "rect");
e.speedY = 1.50; // Sets uniform downward movement speed
display.add(e);
enemies.push(e);
}
// Inside the update loop, check for off-screen boundaries:
if (enemies[i] && enemies[i].y > 650) {
enemies[i].hide();
score -= 12; // Apply points penalty for missing an alien intruder
scoreText.setText("Score: " + score);
enemies.splice(i, 1);
i--;
}
4. Handling Collisions, Custom Explosions, and Camera Shakes
When projectiles strike an enemy structure, we clear the elements and use the particle system to generate an explosive burst, alongside a physical screen shake on the camera view:
if (e.crashWith(bullets[j])) {
// Generate 20 bursting explosion debris rings at the collision coordinates
move.particles.explosion(particles, e.x, e.y, 20);
score += 10;
scoreText.setText("Score: " + score);
// Shakes the viewport canvas horizontally and vertically to signify impact force
display.camera.shake(3, 3);
e.hide();
bullets[j].hide();
enemies.splice(i, 1);
bullets.splice(j, 1);
i--;
break;
}
5. Tracking Lives and Restarting the Game
When a player takes damage, we temporarily subtract a life state and use standard browser alerts combined with structural location commands to instantly wipe parameters and reload the window state:
if (lives <= 0 || score < 0) {
display.stop(); // Terminates active internal frame requests
alert("Game Over! Score: " + score);
location.reload(); // Instantly resets all parameters by reloading the browser tab
}
Full and complete code
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,true);
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].destroy(); 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.destroy(); 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.destroy(); bullets[j].destroy();
enemies.splice(i,1); bullets.splice(j,1);
i--; break;
}
}
if (enemies[i] && enemies[i].y > 650) { enemies[i].destroy();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" });
}