TECH DETAILS

Ski Game - Day 7

Advanced Level

Table of Contents

01 / Infinite Scrolling

The skier stays still; the world scrolls up. This creates the illusion of infinite downhill movement.

const skier = { x: canvas.width / 2, y: 100, // Fixed Y position on screen direction: 0, // -2 to 2 (left to right) speed: 3 // World scroll speed }; function update() { // Skier moves horizontally based on direction skier.x += skier.direction * 3; skier.x = Math.max(30, Math.min(canvas.width - 30, skier.x)); // Move ALL world objects UP (creates downhill effect) for (const obs of obstacles) { obs.y -= skier.speed * 1.5; // Also shift horizontally based on player direction obs.x += skier.direction * 2; } // Track distance traveled distance += skier.speed * 0.3; }
Why Move Objects Instead of Camera?

For infinite games, moving objects toward the player is simpler than managing a camera with infinite scroll.

02 / Obstacle Spawning

Obstacles spawn below the screen and move up. Spawn rate increases with speed.

const obstacleTypes = ['tree', 'tree', 'tree', 'rock', 'rock', 'flag']; function spawnObstacle() { const type = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)]; obstacles.push({ x: Math.random() * (canvas.width - 60) + 30, y: canvas.height + 50, // Start below screen type: type, collected: false // For flags }); } function updateObstacles() { // Remove obstacles that went off screen obstacles = obstacles.filter(obs => obs.y > -50); // Spawn probability increases with speed const spawnChance = 0.03 + (skier.speed * 0.005); if (Math.random() < spawnChance) { spawnObstacle(); } }

03 / Ski Tracks Rendering

Ski tracks show where the player has been. They're stored as points and rendered as lines.

let tracks = []; function updateTracks() { if (!skier.crashed) { // Add current position to tracks tracks.push({ x: skier.x, y: skier.y + 20, // Behind skier direction: skier.direction }); } // Move tracks up with world for (const track of tracks) { track.y -= skier.speed * 1.5; track.x += skier.direction * 2; } // Remove old tracks tracks = tracks.filter(t => t.y > -10); // Limit track count for performance if (tracks.length > 100) tracks.shift(); } function drawTracks() { ctx.strokeStyle = 'rgba(180, 180, 200, 0.5)'; ctx.lineWidth = 2; // Draw two parallel lines (left and right ski) for (let i = 1; i < tracks.length; i++) { const t1 = tracks[i - 1]; const t2 = tracks[i]; // Left ski track ctx.beginPath(); ctx.moveTo(t1.x - 8, t1.y); ctx.lineTo(t2.x - 8, t2.y); ctx.stroke(); // Right ski track ctx.beginPath(); ctx.moveTo(t1.x + 8, t1.y); ctx.lineTo(t2.x + 8, t2.y); ctx.stroke(); } }

04 / Hitbox Collision

Different obstacle types have different hitboxes. Flags can be collected; trees and rocks cause crashes.

function checkCollisions() { const skierBox = { x: skier.x - 10, y: skier.y - 10, width: 20, height: 30 }; for (const obs of obstacles) { // Different hitboxes per type let obsBox; switch (obs.type) { case 'tree': obsBox = { x: obs.x - 15, y: obs.y - 20, width: 30, height: 50 }; break; case 'rock': obsBox = { x: obs.x - 12, y: obs.y - 8, width: 24, height: 18 }; break; case 'flag': obsBox = { x: obs.x - 5, y: obs.y - 15, width: 10, height: 30 }; break; } if (isColliding(skierBox, obsBox)) { if (obs.type === 'flag' && !obs.collected) { // Collect flag - bonus points! obs.collected = true; flags++; distance += 50; } else if (obs.type !== 'flag') { // Crash! crash(); } } } }

05 / High Score with localStorage

Persist the high score across sessions using localStorage.

// Load high score on startup let highScore = parseInt(localStorage.getItem('skiHighScore')) || 0; document.getElementById('highScore').textContent = highScore + 'm'; // Update high score on crash function crash() { skier.crashed = true; gameRunning = false; const finalScore = Math.floor(distance); // Check for new high score if (finalScore > highScore) { highScore = finalScore; localStorage.setItem('skiHighScore', highScore); document.getElementById('highScore').textContent = highScore + 'm'; } showGameOverScreen(finalScore); }
localStorage Limits

localStorage stores strings only. Use parseInt() when reading numbers. Falls back to 0 if not found (|| 0).

06 / Progressive Difficulty

Speed increases over time, making the game progressively harder.

const skier = { speed: 3, // Starting speed maxSpeed: 8 // Cap to prevent impossible speed }; function update() { // Gradually increase speed if (skier.speed < skier.maxSpeed) { skier.speed += 0.001; // Very gradual increase } // Display current speed (for feedback) const displaySpeed = Math.floor(skier.speed * 10); document.getElementById('speed').textContent = displaySpeed; // Obstacles spawn faster at higher speeds (see section 02) // This creates natural difficulty progression } // Direction affects speed perception if (keys.left) { skier.direction = Math.max(skier.direction - 0.15, -2); } else if (keys.right) { skier.direction = Math.min(skier.direction + 0.15, 2); } else { // Return to center gradually if (skier.direction > 0) skier.direction = Math.max(0, skier.direction - 0.1); if (skier.direction < 0) skier.direction = Math.min(0, skier.direction + 0.1); }