Step 9 of 10

Game

📖 Lesson

Browser games

With HTML, CSS and JavaScript you can make games right in the browser! No downloads, no installation.

Basic game ingredients:

  • Game loop - repeated updates (e.g., 60 times per second)
  • Player input - clicks, keyboard, touch
  • Game state - score, time, lives
  • Rendering - displaying current state

Timers and animation

// Timer - run code after 1 second
setTimeout(() => { ... }, 1000);

// Interval - run code every second
setInterval(() => { ... }, 1000);

// Animation loop (60fps)
requestAnimationFrame(update);

For games, timing is key - properly set timers make the game smooth and fun.

Today's game: Catch the target!

Today we'll create a "Catch the target" game:

  • A target appears randomly on screen
  • Click it as fast as you can!
  • You get points for each hit
  • You have limited time - how many points can you score?
  • 3 difficulty levels - the target gets smaller!
You can easily customize this game: change colors, speed, add sounds - just tell AI what you want to change!
💻 Example to try
index.php
🚀 Try on Vibmy
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Catch the Target!</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }

        body {
            min-height: 100vh;
            font-family: "Segoe UI", system-ui, sans-serif;
            background: #0a0a1a;
            color: #e0e0e0;
            overflow: hidden;
        }

        /* Game menu */
        .menu {
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-direction: column;
            background: linear-gradient(135deg, #0f0c29, #1a1a3e, #0f2027);
            text-align: center;
            padding: 2rem;
        }

        .menu h1 {
            font-size: 3rem;
            background: linear-gradient(to right, #00d4aa, #7b68ee, #ff6b9d);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            margin-bottom: 1rem;
        }

        .menu p { color: #8892a4; font-size: 1.1rem; margin-bottom: 2rem; max-width: 500px; }

        .difficulty {
            display: flex;
            gap: 1rem;
            margin-bottom: 2rem;
            flex-wrap: wrap;
            justify-content: center;
        }

        .diff-btn {
            padding: 0.8rem 2rem;
            border: 2px solid rgba(255,255,255,0.15);
            border-radius: 12px;
            background: rgba(255,255,255,0.05);
            color: #e0e0e0;
            font-size: 1rem;
            cursor: pointer;
            transition: all 0.3s;
            font-weight: 600;
        }

        .diff-btn:hover { transform: scale(1.05); }
        .diff-btn.easy { border-color: #00d4aa; color: #00d4aa; }
        .diff-btn.easy:hover { background: rgba(0,212,170,0.2); }
        .diff-btn.medium { border-color: #f59e0b; color: #f59e0b; }
        .diff-btn.medium:hover { background: rgba(245,158,11,0.2); }
        .diff-btn.hard { border-color: #ff6b9d; color: #ff6b9d; }
        .diff-btn.hard:hover { background: rgba(255,107,157,0.2); }

        .highscore { color: #7b68ee; font-size: 0.9rem; margin-top: 1rem; }

        /* Game area */
        .game-area {
            display: none;
            width: 100vw;
            height: 100vh;
            position: relative;
            background: #0a0a1a;
            cursor: crosshair;
        }

        .hud {
            position: absolute;
            top: 0; left: 0; right: 0;
            padding: 1rem 2rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: rgba(0,0,0,0.5);
            backdrop-filter: blur(10px);
            z-index: 10;
        }

        .hud-item {
            font-size: 1.2rem;
            font-weight: 600;
        }

        .hud-score { color: #00d4aa; }
        .hud-time { color: #f59e0b; }
        .hud-combo { color: #ff6b9d; }

        .target {
            position: absolute;
            border-radius: 50%;
            background: radial-gradient(circle, #ff6b9d, #ff1744);
            box-shadow: 0 0 20px rgba(255,107,157,0.5), 0 0 40px rgba(255,107,157,0.2);
            cursor: pointer;
            transition: transform 0.1s;
            z-index: 5;
        }

        .target::before {
            content: "";
            position: absolute;
            top: 50%; left: 50%;
            width: 30%;
            height: 30%;
            background: white;
            border-radius: 50%;
            transform: translate(-50%, -50%);
        }

        .target:active { transform: scale(0.9); }

        .hit-effect {
            position: absolute;
            border-radius: 50%;
            border: 2px solid #00d4aa;
            animation: ripple 0.6s ease-out forwards;
            pointer-events: none;
            z-index: 4;
        }

        .score-popup {
            position: absolute;
            color: #00d4aa;
            font-weight: 800;
            font-size: 1.5rem;
            animation: scoreFloat 0.8s ease-out forwards;
            pointer-events: none;
            z-index: 6;
        }

        .miss-effect {
            position: absolute;
            color: #ff6b9d;
            font-size: 1.2rem;
            animation: scoreFloat 0.5s ease-out forwards;
            pointer-events: none;
            z-index: 6;
        }

        @keyframes ripple {
            from { width: 20px; height: 20px; opacity: 1; transform: translate(-50%, -50%); }
            to { width: 100px; height: 100px; opacity: 0; transform: translate(-50%, -50%); }
        }

        @keyframes scoreFloat {
            from { opacity: 1; transform: translateY(0); }
            to { opacity: 0; transform: translateY(-50px); }
        }

        /* Game over */
        .game-over {
            display: none;
            position: fixed;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(10,10,26,0.95);
            z-index: 20;
            align-items: center;
            justify-content: center;
            flex-direction: column;
            text-align: center;
        }

        .game-over.show { display: flex; }

        .final-score {
            font-size: 5rem;
            font-weight: 800;
            background: linear-gradient(to right, #00d4aa, #7b68ee);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            animation: popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
        }

        .final-label { color: #8892a4; font-size: 1.2rem; margin-bottom: 0.5rem; }
        .final-stats { color: #6b7280; margin: 1rem 0 2rem; }
        .final-new { color: #f59e0b; font-weight: 600; margin-bottom: 1rem; font-size: 1.1rem; }

        .play-again {
            padding: 1rem 3rem;
            background: linear-gradient(135deg, #00d4aa, #7b68ee);
            color: white;
            border: none;
            border-radius: 12px;
            font-size: 1.1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s;
        }

        .play-again:hover { transform: scale(1.1); }

        .back-menu {
            margin-top: 1rem;
            padding: 0.6rem 1.5rem;
            background: none;
            border: 1px solid rgba(255,255,255,0.2);
            color: #888;
            border-radius: 10px;
            cursor: pointer;
            font-size: 0.9rem;
            transition: all 0.3s;
        }

        .back-menu:hover { border-color: #fff; color: #fff; }

        @keyframes popIn { from { transform: scale(0); } to { transform: scale(1); } }
    </style>
</head>
<body>
    <!-- MENU -->
    <div class="menu" id="menu">
        <h1>&#x1F3AF; Catch the Target!</h1>
        <p>Click the red targets as fast as you can! You have 30 seconds. How many points can you score?</p>

        <div class="difficulty">
            <button class="diff-btn easy" onclick="startGame('easy')">&#x1F7E2; Easy</button>
            <button class="diff-btn medium" onclick="startGame('medium')">&#x1F7E1; Medium</button>
            <button class="diff-btn hard" onclick="startGame('hard')">&#x1F534; Hard</button>
        </div>

        <div class="highscore" id="highscoreDisplay"></div>
    </div>

    <!-- GAME AREA -->
    <div class="game-area" id="gameArea" onclick="handleMiss(event)">
        <div class="hud">
            <div class="hud-item hud-score" id="hudScore">&#x1F3AF; 0</div>
            <div class="hud-item hud-combo" id="hudCombo"></div>
            <div class="hud-item hud-time" id="hudTime">&#x23F1; 30s</div>
        </div>
    </div>

    <!-- GAME OVER -->
    <div class="game-over" id="gameOver">
        <div class="final-label">Final Score</div>
        <div class="final-score" id="finalScore">0</div>
        <div class="final-new" id="finalNew" style="display:none">&#x1F3C6; New record!</div>
        <div class="final-stats" id="finalStats"></div>
        <button class="play-again" onclick="restartGame()">&#x1F504; Play Again</button>
        <button class="back-menu" onclick="backToMenu()">&#x2190; Menu</button>
    </div>

    <script>
        let score = 0, timeLeft = 30, combo = 0, maxCombo = 0, hits = 0, misses = 0;
        let difficulty = "easy";
        let gameInterval, targetTimeout, currentTarget;
        const sizes = { easy: 70, medium: 50, hard: 30 };
        const speeds = { easy: 1500, medium: 1000, hard: 700 };

        function getHighscores() {
            try { return JSON.parse(localStorage.getItem("targetgame_hs") || "{}"); }
            catch { return {}; }
        }

        function showHighscores() {
            const hs = getHighscores();
            const parts = [];
            if (hs.easy) parts.push("Easy: " + hs.easy);
            if (hs.medium) parts.push("Medium: " + hs.medium);
            if (hs.hard) parts.push("Hard: " + hs.hard);
            document.getElementById("highscoreDisplay").textContent = parts.length ? "&#x1F3C6; " + parts.join(" | ") : "";
        }

        showHighscores();

        function startGame(diff) {
            difficulty = diff;
            score = 0; timeLeft = 30; combo = 0; maxCombo = 0; hits = 0; misses = 0;

            document.getElementById("menu").style.display = "none";
            document.getElementById("gameArea").style.display = "block";
            document.getElementById("gameOver").classList.remove("show");

            updateHUD();
            spawnTarget();

            gameInterval = setInterval(() => {
                timeLeft--;
                updateHUD();
                if (timeLeft <= 0) endGame();
            }, 1000);
        }

        function updateHUD() {
            document.getElementById("hudScore").textContent = "&#x1F3AF; " + score;
            document.getElementById("hudTime").textContent = "&#x23F1; " + timeLeft + "s";
            document.getElementById("hudCombo").textContent = combo > 1 ? "&#x1F525; x" + combo : "";
        }

        function spawnTarget() {
            if (currentTarget) currentTarget.remove();

            const size = sizes[difficulty];
            const area = document.getElementById("gameArea");
            const maxX = area.clientWidth - size - 20;
            const maxY = area.clientHeight - size - 70;
            const x = Math.random() * maxX + 10;
            const y = Math.random() * maxY + 60;

            const target = document.createElement("div");
            target.className = "target";
            target.style.width = size + "px";
            target.style.height = size + "px";
            target.style.left = x + "px";
            target.style.top = y + "px";
            target.onclick = function(e) {
                e.stopPropagation();
                hitTarget(this);
            };

            area.appendChild(target);
            currentTarget = target;

            targetTimeout = setTimeout(() => {
                if (currentTarget === target) {
                    combo = 0;
                    spawnTarget();
                }
            }, speeds[difficulty] + 500);
        }

        function hitTarget(target) {
            hits++;
            combo++;
            if (combo > maxCombo) maxCombo = combo;

            const points = 10 * combo;
            score += points;

            const rect = target.getBoundingClientRect();
            const cx = rect.left + rect.width / 2;
            const cy = rect.top + rect.height / 2;

            // Hit effect
            const ripple = document.createElement("div");
            ripple.className = "hit-effect";
            ripple.style.left = cx + "px";
            ripple.style.top = cy + "px";
            document.getElementById("gameArea").appendChild(ripple);
            setTimeout(() => ripple.remove(), 600);

            // Score popup
            const popup = document.createElement("div");
            popup.className = "score-popup";
            popup.textContent = "+" + points + (combo > 1 ? " (x" + combo + ")" : "");
            popup.style.left = cx + "px";
            popup.style.top = (cy - 20) + "px";
            document.getElementById("gameArea").appendChild(popup);
            setTimeout(() => popup.remove(), 800);

            clearTimeout(targetTimeout);
            target.remove();
            currentTarget = null;
            updateHUD();

            setTimeout(spawnTarget, 100);
        }

        function handleMiss(e) {
            if (e.target.classList.contains("target")) return;
            misses++;
            combo = 0;
            updateHUD();

            const miss = document.createElement("div");
            miss.className = "miss-effect";
            miss.textContent = "Miss!";
            miss.style.left = e.clientX + "px";
            miss.style.top = e.clientY + "px";
            document.getElementById("gameArea").appendChild(miss);
            setTimeout(() => miss.remove(), 500);
        }

        function endGame() {
            clearInterval(gameInterval);
            clearTimeout(targetTimeout);
            if (currentTarget) currentTarget.remove();

            document.getElementById("finalScore").textContent = score;
            const accuracy = hits + misses > 0 ? Math.round(hits / (hits + misses) * 100) : 0;
            document.getElementById("finalStats").innerHTML =
                "Hits: " + hits + " | Misses: " + misses +
                " | Accuracy: " + accuracy + "%" +
                " | Max combo: x" + maxCombo;

            const hs = getHighscores();
            const isNew = !hs[difficulty] || score > hs[difficulty];
            if (isNew && score > 0) {
                hs[difficulty] = score;
                localStorage.setItem("targetgame_hs", JSON.stringify(hs));
            }
            document.getElementById("finalNew").style.display = isNew && score > 0 ? "block" : "none";

            document.getElementById("gameOver").classList.add("show");
        }

        function restartGame() { startGame(difficulty); }

        function backToMenu() {
            document.getElementById("gameArea").style.display = "none";
            document.getElementById("gameOver").classList.remove("show");
            document.getElementById("menu").style.display = "flex";
            showHighscores();
        }
    </script>
</body>
</html>
← Back 9 / 10 Next step →