🕹️ 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.
📋 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
mkdir puua-objekt
cd puua-objekt
git init
touch index.html style.css game.js
<!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.
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.
// ===== 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:
// 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!
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 aegaperformance.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.