MOODUL 03 · TASE 1 — ALGAJA

🕹️ Esimene 2D Mäng — "Püüa Objekt"

Ehita oma esimene täielik mäng algusest lõpuni! Mängija liigub, objektid kukuvad, skoor kasvab, raskusaste tõuseb ja high score salvestatakse.

⏱️ ~4-5 tundi 📝 7 harjutust 🟢 Algaja

📋 Eeldused

ℹ️

Peaksid olema läbinud Moodul 02 — Canvas, animatsioon ja klaviatuuri sisend peavad olema tuttavad.

🎯 Mida Sa Ehitad

Selle mooduli jooksul ehitad täieliku mängu kus:

  • 🎮 Mängija liigub vasakule/paremale nooleklahvidega
  • ⭐ Taevast kukuvad mündid/tähhed mida tuleb püüda
  • 💀 Kukuvad ka ohtlikud objektid mida tuleb vältida
  • 📊 Skoorisüsteem — kogutud mündid = punktid
  • ❤️ Elusüsteem — 3 elu, ohtlik objekt = -1 elu
  • 📈 Raskusaste tõuseb aja jooksul
  • 🏆 High score salvestatakse localStorage'isse
  • 📋 Start menüü ja Game Over ekraan

📦 Samm 1 — Projekti Seadistamine

Terminal
mkdir puua-objekt
cd puua-objekt
git init

touch index.html style.css game.js
index.html
<!DOCTYPE html>
<html lang="et">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>🎮 Püüa Objekt!</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <canvas id="game" width="600" height="700"></canvas>
  <script src="game.js"></script>
</body>
</html>
💡

Commit kohe! git add . && git commit -m "feat: initsialiseeri projekt"

🏃 Samm 2 — Game Loop ja Mängija

Alustame game loop'iga ja lisame mängija tegelase, mis liigub vasakule ja paremale.

game.js — Game Loop + Mängija
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

// ===== MÄNGU OLEK =====
const GameState = { MENU: 'menu', PLAYING: 'playing', GAMEOVER: 'gameover' };
let state = GameState.MENU;

// ===== MÄNGIJA =====
const player = {
  width: 60,
  height: 20,
  x: 0,
  y: 0,
  speed: 7,
  color: '#22c55e'
};

// Paiguta mängija alumisele osale
player.x = (canvas.width - player.width) / 2;
player.y = canvas.height - 50;

// ===== SISEND =====
const keys = {};
window.addEventListener('keydown', (e) => { keys[e.key] = true; });
window.addEventListener('keyup', (e) => { keys[e.key] = false; });

// ===== UUENDAMINE =====
function update() {
  if (state !== GameState.PLAYING) return;

  // Mängija liikumine
  if (keys['ArrowLeft'] || keys['a'])  player.x -= player.speed;
  if (keys['ArrowRight'] || keys['d']) player.x += player.speed;

  // Ekraani piirid
  player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));
}

// ===== JOONISTAMINE =====
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  if (state === GameState.MENU) {
    drawMenu();
  } else if (state === GameState.PLAYING) {
    drawGame();
  } else if (state === GameState.GAMEOVER) {
    drawGameOver();
  }
}

function drawMenu() {
  ctx.fillStyle = '#e2e8f0';
  ctx.font = 'bold 36px Inter, sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('🎮 Püüa Objekt!', canvas.width / 2, 250);

  ctx.font = '18px Inter, sans-serif';
  ctx.fillStyle = '#94a3b8';
  ctx.fillText('Püüa münde ⭐ ja väldi pomme 💀', canvas.width / 2, 300);

  ctx.fillStyle = '#06b6d4';
  ctx.font = 'bold 20px Inter, sans-serif';
  ctx.fillText('Vajuta ENTER alustamiseks', canvas.width / 2, 400);
}

function drawGame() {
  // Mängija
  ctx.fillStyle = player.color;
  ctx.fillRect(player.x, player.y, player.width, player.height);

  // Silmad (et mängija oleks armsam)
  ctx.fillStyle = '#fff';
  ctx.fillRect(player.x + 15, player.y + 5, 8, 8);
  ctx.fillRect(player.x + 37, player.y + 5, 8, 8);
  ctx.fillStyle = '#000';
  ctx.fillRect(player.x + 18, player.y + 7, 4, 4);
  ctx.fillRect(player.x + 40, player.y + 7, 4, 4);
}

function drawGameOver() {
  ctx.fillStyle = '#ef4444';
  ctx.font = 'bold 40px Inter, sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('GAME OVER', canvas.width / 2, 280);

  ctx.fillStyle = '#06b6d4';
  ctx.font = '18px Inter, sans-serif';
  ctx.fillText('Vajuta ENTER uuesti mängimiseks', canvas.width / 2, 400);
}

// ===== MENÜÜ INPUT =====
window.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    if (state === GameState.MENU || state === GameState.GAMEOVER) {
      startGame();
    }
  }
});

function startGame() {
  state = GameState.PLAYING;
  player.x = (canvas.width - player.width) / 2;
}

// ===== GAME LOOP =====
function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}

gameLoop();

⭐ Samm 3 — Kukuvad Objektid

Lisame mündid (⭐) ja pommid (💀) mis kukuvad alla. Kasutame massiivi objektide hoidmiseks.

game.js — Lisa enne "MENÜÜ INPUT" sektsiooni
// ===== KUKUVAD OBJEKTID =====
let fallingObjects = [];
let spawnTimer = 0;
let spawnInterval = 60; // Kaadrit objektide vahel (väiksem = tihedam)
let score = 0;
let lives = 3;
let gameTime = 0;

function spawnObject() {
  const isCoin = Math.random() > 0.3; // 70% mündid, 30% pommid

  fallingObjects.push({
    x: Math.random() * (canvas.width - 30),
    y: -30,
    width: 25,
    height: 25,
    speed: 2 + Math.random() * 2,
    type: isCoin ? 'coin' : 'bomb'
  });
}

function updateObjects() {
  spawnTimer++;
  gameTime++;

  // Raskusaste tõuseb aja jooksul
  if (gameTime % 600 === 0 && spawnInterval > 20) {
    spawnInterval -= 5;
  }

  if (spawnTimer >= spawnInterval) {
    spawnObject();
    spawnTimer = 0;
  }

  for (let i = fallingObjects.length - 1; i >= 0; i--) {
    const obj = fallingObjects[i];
    obj.y += obj.speed;

    // Collision detection (AABB)
    if (
      player.x < obj.x + obj.width &&
      player.x + player.width > obj.x &&
      player.y < obj.y + obj.height &&
      player.y + player.height > obj.y
    ) {
      if (obj.type === 'coin') {
        score += 10;
      } else {
        lives--;
        if (lives <= 0) {
          gameOver();
        }
      }
      fallingObjects.splice(i, 1);
      continue;
    }

    // Eemalda kui kukkus ekraanilt välja
    if (obj.y > canvas.height) {
      fallingObjects.splice(i, 1);
    }
  }
}

function drawObjects() {
  fallingObjects.forEach(obj => {
    ctx.font = '22px sans-serif';
    ctx.fillText(obj.type === 'coin' ? '⭐' : '💀', obj.x, obj.y + 20);
  });
}

function drawHUD() {
  ctx.textAlign = 'left';
  ctx.font = 'bold 18px Inter, sans-serif';
  ctx.fillStyle = '#eab308';
  ctx.fillText('⭐ ' + score, 15, 30);

  ctx.fillStyle = '#ef4444';
  ctx.fillText('❤️ '.repeat(lives), 15, 55);

  // High score
  const highScore = localStorage.getItem('puua-highscore') || 0;
  ctx.textAlign = 'right';
  ctx.fillStyle = '#94a3b8';
  ctx.font = '14px Inter, sans-serif';
  ctx.fillText('Parim: ' + highScore, canvas.width - 15, 30);
  ctx.textAlign = 'left';
}

function gameOver() {
  state = GameState.GAMEOVER;

  // Salvesta high score
  const highScore = parseInt(localStorage.getItem('puua-highscore') || 0);
  if (score > highScore) {
    localStorage.setItem('puua-highscore', score);
  }
}

Nüüd uuenda update(), drawGame() ja startGame() funktsioone:

game.js — Uuendatud funktsioonid
// Uuendatud update()
function update() {
  if (state !== GameState.PLAYING) return;

  if (keys['ArrowLeft'] || keys['a'])  player.x -= player.speed;
  if (keys['ArrowRight'] || keys['d']) player.x += player.speed;
  player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));

  updateObjects(); // LISA SEE RIDA
}

// Uuendatud drawGame()
function drawGame() {
  drawObjects();  // LISA SEE

  // Mängija
  ctx.fillStyle = player.color;
  ctx.fillRect(player.x, player.y, player.width, player.height);
  ctx.fillStyle = '#fff';
  ctx.fillRect(player.x + 15, player.y + 5, 8, 8);
  ctx.fillRect(player.x + 37, player.y + 5, 8, 8);
  ctx.fillStyle = '#000';
  ctx.fillRect(player.x + 18, player.y + 7, 4, 4);
  ctx.fillRect(player.x + 40, player.y + 7, 4, 4);

  drawHUD();  // LISA SEE
}

// Uuendatud drawGameOver()
function drawGameOver() {
  ctx.fillStyle = '#ef4444';
  ctx.font = 'bold 40px Inter, sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('GAME OVER', canvas.width / 2, 250);

  ctx.fillStyle = '#eab308';
  ctx.font = '24px Inter, sans-serif';
  ctx.fillText('Skoor: ' + score, canvas.width / 2, 310);

  const highScore = localStorage.getItem('puua-highscore') || 0;
  ctx.fillStyle = '#94a3b8';
  ctx.font = '18px Inter, sans-serif';
  ctx.fillText('Parim tulemus: ' + highScore, canvas.width / 2, 350);

  ctx.fillStyle = '#06b6d4';
  ctx.fillText('Vajuta ENTER uuesti mängimiseks', canvas.width / 2, 420);
  ctx.textAlign = 'left';
}

// Uuendatud startGame()
function startGame() {
  state = GameState.PLAYING;
  player.x = (canvas.width - player.width) / 2;
  fallingObjects = [];
  score = 0;
  lives = 3;
  gameTime = 0;
  spawnTimer = 0;
  spawnInterval = 60;
}
⚠️

AABB Collision Detection (Axis-Aligned Bounding Box) kontrollib kas kaks ristkülikut kattuvad. See on lihtne ja kiire — sobib enamikule 2D mängudele!

🔀 Commiti Oma Progress!

Terminal
git add .
git commit -m "feat: lisa kukuvad objektid, collision, skoor ja elusüsteem"
git push

🧑‍🚀 Iseseisev Projekt — "Kosmose Vältimine"

🚀

Sama loogika, teine teema! Ehita mäng kus kosmoselaev (mängija) väldib asteroide. Lisa: tausta tähed (liikuvad alla), lasertulistamine (tühik), vaenlase hävitamine = punktid. Kasuta kõike mida siin õppisid!

📝 Harjutused

  • Game Loop ja FPS loendur

    Ehita game loop requestAnimationFrame-iga ja kuva ekraanil FPS number (kaadreid sekundis). Vihje: loe aega performance.now()-ga.

  • Mängija liikumine

    Loo mängija kes liigub vasakule/paremale nooleklahvidega. Mängija ei tohi ekraanilt välja minna.

  • Kukuvad objektid

    Lisa kukuvad objektid — iga 2 sekundi tagant tekib uus juhuslikust positsioonist. Objektid kukuvad erineva kiirusega.

  • AABB Collision Detection

    Implementeeri AABB collision detection — mängija ja objekt kokkupõrge. Lisa visuaalne tagasiside (välgatus).

  • Skoor ja elusüsteem

    Lisa skoor (kogutud mündid) ja elud (3 elu, pomm = -1). Kuva mõlemad ekraanil (HUD).

  • Raskusastme tõus

    Mida kauem mängija mängib, seda kiiremini tekivad uued objektid ja seda kiiremini kukuvad. Lisa visuaalne "TASE TÕUSIS!" teade.

  • Menüü, Game Over, High Score

    Lisa kolm ekraani: start menüü, mäng ise, game over. Salvesta high score localStorage-isse. Lisa "Proovi uuesti" nupp.

← Moodul 02: Canvas & JS Vaata Kõiki Mooduleid →