🎮 Практическая работа №3

Создаём игру «Арканоид» с нуля (очень подробно)
🎯 Цель: Сделать полноценную игру: мяч отбивается ракеткой, разбивает кирпичи, начисляются очки, даются три жизни. Управление — стрелки и мышь.
📖 Как пользоваться инструкцией: Каждый шаг содержит код, который нужно скопировать в ваш файл game.html. Я объясняю, куда именно вставлять и что каждая строчка делает.

Управление: стрелки влево/вправо или мышь

📁 Шаг 1. Создаём файл и базовую разметку

Создай новый файл game.html и скопируй в него этот код:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Арканоид — моя игра</title>
    <style>
        * { padding: 0; margin: 0; box-sizing: border-box; }
        canvas {
            background: #eee;
            display: block;
            margin: 20px auto;
            box-shadow: 0 0 10px rgba(0,0,0,0.3);
        }
        body {
            background: linear-gradient(135deg, #1a1a2e, #16213e);
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }
        .controls { text-align: center; margin-top: 10px; }
        button {
            background: linear-gradient(135deg, #ff6b6b, #feca57);
            color: #1a1a2e;
            padding: 10px 24px;
            border-radius: 30px;
            font-size: 16px;
            font-weight: bold;
            border: none;
            cursor: pointer;
            margin: 5px;
        }
        button:hover { transform: scale(1.05); }
        .status {
            text-align: center;
            margin-top: 10px;
            color: #feca57;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <canvas id="gameCanvas" width="480" height="320"></canvas>
    <div class="controls">
        <button id="startBtn">🎮 Начать игру</button>
        <button id="restartBtn">🔄 Начать заново</button>
    </div>
    <div id="gameStatus" class="status">⚡ Нажми «Начать игру»</div>
    <script>
        // Весь код игры будем писать здесь
    </script>
</body>
</html>
📌 Что здесь происходит:

⚽ Шаг 2. Рисуем мяч и заставляем его двигаться

Вставь этот код внутрь тега <script> (после комментария "// Весь код игры будем писать здесь"):

// ========== 1. Получаем доступ к холсту ==========
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

// ========== 2. Переменные для управления игрой ==========
let gameRunning = false;   // false — игра стоит на паузе, true — мяч движется

// ========== 3. Параметры мяча ==========
let x = canvas.width / 2;      // x-координата центра мяча (по горизонтали) — ставим по середине
let y = canvas.height - 30;    // y-координата (по вертикали) — 30 пикселей от низа
let dx = 2;                    // скорость по горизонтали (2 пикселя за кадр, положительное — вправо)
let dy = -2;                   // скорость по вертикали (отрицательная — вверх)
const ballRadius = 8;          // радиус мяча (8 пикселей)

// ========== 4. Функция рисования мяча ==========
function drawBall() {
    ctx.beginPath();                      // начинаем новую фигуру
    ctx.arc(x, y, ballRadius, 0, Math.PI * 2);  // рисуем круг (центр x,y, радиус, угол от 0 до 360°)
    ctx.fillStyle = "#0095DD";            // цвет заливки — синий
    ctx.fill();                           // заливаем
    ctx.closePath();                      // завершаем фигуру
}

// ========== 5. Главная функция анимации ==========
function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height); // очищаем весь холст
    drawBall();                                       // рисуем мяч

    if (gameRunning) {
        // Отскок от левой и правой стены
        if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
            dx = -dx;   // разворачиваем горизонтальную скорость
        }
        // Отскок от верхней стены
        if (y + dy < ballRadius) {
            dy = -dy;   // разворачиваем вертикальную скорость
        }

        // Обновляем позицию мяча
        x += dx;
        y += dy;
    }

    requestAnimationFrame(draw);  // просим браузер вызвать эту функцию снова (60 раз в секунду)
}

draw();  // запускаем анимацию (мяч пока не движется, так как gameRunning = false)

// ========== 6. Кнопка "Начать игру" ==========
document.getElementById('startBtn').addEventListener('click', () => {
    gameRunning = true;
});
📌 Объяснение:
  • canvas.getContext('2d') — получаем инструмент для рисования.
  • requestAnimationFrame(draw) — создаёт бесконечный цикл перерисовки (анимацию).
  • gameRunning — флаг, который разрешает движение мяча. По нажатию кнопки «Старт» он становится true.
  • Отскок от стен: если мяч выходит за границы, меняем знак скорости на противоположный.

Проверь: Открой файл в браузере. Мяч должен стоять на месте. Нажми «Начать игру» — мяч полетит и будет отскакивать от левой, правой и верхней стен. От нижней стены он пока не отскакивает — это сделаем на следующем шаге.

🏓 Шаг 3. Добавляем ракетку и управление

Добавь этот код в конец скрипта (перед закрывающим тегом </script>):

// ========== 7. Параметры ракетки ==========
let paddleHeight = 10;          // высота ракетки
let paddleWidth = 75;           // ширина ракетки
let paddleX = (canvas.width - paddleWidth) / 2;  // начальное положение — по центру
let rightPressed = false;       // флаг "нажата стрелка вправо"
let leftPressed = false;        // флаг "нажата стрелка влево"

// ========== 8. Управление с клавиатуры ==========
document.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowRight') rightPressed = true;
    if (e.key === 'ArrowLeft') leftPressed = true;
});
document.addEventListener('keyup', (e) => {
    if (e.key === 'ArrowRight') rightPressed = false;
    if (e.key === 'ArrowLeft') leftPressed = false;
});

// ========== 9. Управление мышью ==========
canvas.addEventListener('mousemove', (e) => {
    if (!gameRunning) return;   // двигать ракетку можно только когда игра идёт
    const mouseX = e.clientX - canvas.offsetLeft; // координата мыши внутри canvas
    if (mouseX > 0 && mouseX < canvas.width) {
        paddleX = mouseX - paddleWidth / 2;       // ставим ракетку под курсор
        // Не даём ракетке вылезти за левый край
        if (paddleX < 0) paddleX = 0;
        // Не даём вылезти за правый край
        if (paddleX > canvas.width - paddleWidth) paddleX = canvas.width - paddleWidth;
    }
});

// ========== 10. Рисуем ракетку ==========
function drawPaddle() {
    ctx.beginPath();
    ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
    ctx.fillStyle = "#0095DD";
    ctx.fill();
    ctx.closePath();
}

// ========== 11. Добавляем движение ракетки и отскок от неё в функцию draw() ==========
// Нужно изменить функцию draw(). Ниже показан полный код draw() с новыми вставками.
// Замени свою старую функцию draw() на эту:
function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawBall();
    drawPaddle();               // ← рисуем ракетку

    if (gameRunning) {
        // Движение ракетки по клавишам
        if (rightPressed && paddleX < canvas.width - paddleWidth) paddleX += 7;
        if (leftPressed && paddleX > 0) paddleX -= 7;

        // Отскок от левой/правой стены
        if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) dx = -dx;
        // Отскок от верхней стены
        if (y + dy < ballRadius) dy = -dy;

        // ОТСКОК ОТ РАКЕТКИ
        if (y + dy > canvas.height - ballRadius - paddleHeight &&
            y + dy < canvas.height - ballRadius &&
            x > paddleX && x < paddleX + paddleWidth) {
            dy = -dy;
        }

        // Если мяч упал ниже ракетки (проигрыш) — возвращаем в центр и уменьшаем жизнь (позже)
        if (y + dy > canvas.height - ballRadius) {
            // Пока просто возвращаем мяч в центр (без потери жизни)
            x = canvas.width / 2;
            y = canvas.height - 30;
            dx = 2;
            dy = -2;
        }

        x += dx;
        y += dy;
    }

    requestAnimationFrame(draw);
}

// Перезапускаем draw (новая версия уже работает)
📌 Объяснение:
  • paddleX — x-координата левого верхнего угла ракетки.
  • rightPressed/leftPressed — запоминают, нажата ли стрелка.
  • В функции draw() добавляем drawPaddle() и движение ракетки.
  • Отскок от ракетки: проверяем, что мяч находится на уровне ракетки и по горизонтали попадает в её границы.
  • Если мяч упал ниже ракетки — просто возвращаем его в центр (в следующем шаге добавим счётчик жизней).

Проверь: Теперь ракетка двигается за мышью и стрелками, мяч отскакивает от неё. Если пропустишь мяч — он вернётся в центр.

🧱 Шаг 4. Создаём кирпичи

Добавь этот код после объявления переменных мяча (например, после строки "let ballRadius = 8;"):

// ========== 12. Параметры кирпичей ==========
const brickRowCount = 3;        // количество строк кирпичей
const brickColumnCount = 5;     // количество столбцов
const brickWidth = 75;          // ширина одного кирпича
const brickHeight = 15;         // высота
const brickPadding = 10;        // отступ между кирпичами
const brickOffsetTop = 30;      // отступ от верхнего края
const brickOffsetLeft = 30;     // отступ от левого края

let bricks = [];                // массив, где будем хранить кирпичи

// ========== 13. Функция инициализации кирпичей (создаёт массив) ==========
function initBricks() {
    bricks = [];
    for (let c = 0; c < brickColumnCount; c++) {
        bricks[c] = [];
        for (let r = 0; r < brickRowCount; r++) {
            bricks[c][r] = { x: 0, y: 0, status: 1 };  // status: 1 = целый, 0 = разбит
        }
    }
}

// ========== 14. Рисуем кирпичи ==========
function drawBricks() {
    for (let c = 0; c < brickColumnCount; c++) {
        for (let r = 0; r < brickRowCount; r++) {
            if (bricks[c][r].status === 1) {  // рисуем только целые
                const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
                const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
                bricks[c][r].x = brickX;
                bricks[c][r].y = brickY;
                ctx.beginPath();
                ctx.rect(brickX, brickY, brickWidth, brickHeight);
                ctx.fillStyle = "#0095DD";
                ctx.fill();
                ctx.closePath();
            }
        }
    }
}

// Вызови initBricks() один раз при загрузке (добавь после объявления функций)
initBricks();
📌 Объяснение:
  • Кирпичи располагаются в виде сетки 5×3. Каждый кирпич — прямоугольник.
  • status хранит, разбит кирпич (0) или нет (1).
  • initBricks() создаёт новый массив со всеми целыми кирпичами.
  • Не забудь добавить вызов drawBricks() внутрь функции draw() (сделаем на следующем шаге).

💥 Шаг 5. Обнаружение столкновений мяча с кирпичами

Добавь эту функцию после функции drawBricks():

// ========== 15. Проверка попадания в кирпичи ==========
function collisionDetection() {
    for (let c = 0; c < brickColumnCount; c++) {
        for (let r = 0; r < brickRowCount; r++) {
            const b = bricks[c][r];
            if (b.status === 1) {
                // Если мяч внутри прямоугольника кирпича
                if (x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
                    dy = -dy;          // отскакивает
                    b.status = 0;      // кирпич разбит
                    score++;           // увеличиваем счёт (переменную объявим позже)
                    // Если разбили все кирпичи — победа
                    if (score === brickRowCount * brickColumnCount) {
                        gameRunning = false;
                        updateStatusMessage("🎉 ПОБЕДА! 🎉");
                    }
                }
            }
        }
    }
}

Теперь нужно обновить функцию draw(): добавить в неё отрисовку кирпичей и вызов collisionDetection(). Также добавим переменную score и функцию для вывода счёта.

Добавь эти переменные в начало скрипта (рядом с переменными мяча):

let score = 0;      // очки

Добавь функцию для рисования счёта:

function drawScore() {
    ctx.font = "16px Arial";
    ctx.fillStyle = "#0095DD";
    ctx.fillText("Score: " + score, 8, 20);
}

Добавь функцию для сообщений (обновляет текст в #gameStatus):

function updateStatusMessage(msg) {
    const statusDiv = document.getElementById('gameStatus');
    if (statusDiv) statusDiv.textContent = msg;
}

Теперь замени функцию draw() на эту (полностью):

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawBricks();          // рисуем кирпичи
    drawBall();
    drawPaddle();
    drawScore();           // рисуем счёт

    if (gameRunning) {
        collisionDetection();   // проверяем столкновения с кирпичами

        if (rightPressed && paddleX < canvas.width - paddleWidth) paddleX += 7;
        if (leftPressed && paddleX > 0) paddleX -= 7;

        if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) dx = -dx;
        if (y + dy < ballRadius) dy = -dy;

        if (y + dy > canvas.height - ballRadius - paddleHeight &&
            y + dy < canvas.height - ballRadius &&
            x > paddleX && x < paddleX + paddleWidth) {
            dy = -dy;
        }

        if (y + dy > canvas.height - ballRadius) {
            // Потеря жизни — добавим позже, пока просто сбрасываем позицию
            x = canvas.width / 2;
            y = canvas.height - 30;
            dx = 2;
            dy = -2;
        }

        x += dx;
        y += dy;
    }

    requestAnimationFrame(draw);
}

Проверь: Теперь на экране появились кирпичи, и мяч их разбивает. Счёт увеличивается. При разбитии всех кирпичей игра останавливается и выводится "ПОБЕДА".

🏆 Шаг 6. Добавляем жизни и кнопку перезапуска

Добавь переменную lives (жизни) рядом с другими переменными:

let lives = 3;      // начальное количество жизней

Функция для отображения жизней:

function drawLives() {
    ctx.font = "16px Arial";
    ctx.fillStyle = "#0095DD";
    ctx.fillText("Lives: " + lives, canvas.width - 65, 20);
}

Добавь вызов drawLives() в функцию draw() (после drawScore()).

Теперь изменим обработку потери жизни в функции draw(). Найди место, где было if (y + dy > canvas.height - ballRadius) и замени его на:

if (y + dy > canvas.height - ballRadius) {
    lives--;
    if (lives === 0) {
        gameRunning = false;
        updateStatusMessage("💀 GAME OVER 💀 Очки: " + score);
        // остановить анимацию (можно оставить, но игра не двигается)
    } else {
        // Сброс позиции мяча и ракетки
        x = canvas.width / 2;
        y = canvas.height - 30;
        dx = 2;
        dy = -2;
        paddleX = (canvas.width - paddleWidth) / 2;
        updateStatusMessage("❤️ Жизнь потеряна! Осталось: " + lives);
    }
}

Наконец, кнопка "Начать заново" — добавь этот код в конец скрипта:

function restartGame() {
    gameRunning = true;
    lives = 3;
    score = 0;
    initBricks();               // восстанавливаем все кирпичи
    x = canvas.width / 2;
    y = canvas.height - 30;
    dx = 2;
    dy = -2;
    paddleX = (canvas.width - paddleWidth) / 2;
    updateStatusMessage("🎮 Игра перезапущена! Удачи!");
}

document.getElementById('restartBtn').addEventListener('click', () => {
    restartGame();
});
📌 Итог: Теперь у вас полноценная игра: мяч отскакивает от ракетки, разбивает кирпичи, начисляются очки, есть три жизни, кнопка перезапуска. Управление — стрелки или мышь.

📄 Полный итоговый код (скопируй в один файл)

Если в процессе что-то пошло не так, вот полный работающий код. Сохрани его как game.html и открой в браузере.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Арканоид — моя игра</title>
    <style>
        * { padding: 0; margin: 0; box-sizing: border-box; }
        canvas { background: #eee; display: block; margin: 20px auto; box-shadow: 0 0 10px rgba(0,0,0,0.3); }
        body { background: linear-gradient(135deg, #1a1a2e, #16213e); min-height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; }
        .controls { text-align: center; margin-top: 10px; }
        button { background: linear-gradient(135deg, #ff6b6b, #feca57); color: #1a1a2e; padding: 10px 24px; border-radius: 30px; font-size: 16px; font-weight: bold; border: none; cursor: pointer; margin: 5px; }
        button:hover { transform: scale(1.05); }
        .status { text-align: center; margin-top: 10px; color: #feca57; font-size: 14px; }
    </style>
</head>
<body>
    <canvas id="gameCanvas" width="480" height="320"></canvas>
    <div class="controls">
        <button id="startBtn">🎮 Начать игру</button>
        <button id="restartBtn">🔄 Начать заново</button>
    </div>
    <div id="gameStatus" class="status">⚡ Нажми «Начать игру»</div>
    <script>
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');

        let gameRunning = false;

        // Мяч
        let x = canvas.width / 2;
        let y = canvas.height - 30;
        let dx = 2;
        let dy = -2;
        const ballRadius = 8;

        // Ракетка
        let paddleHeight = 10;
        let paddleWidth = 75;
        let paddleX = (canvas.width - paddleWidth) / 2;
        let rightPressed = false;
        let leftPressed = false;

        // Кирпичи
        const brickRowCount = 3;
        const brickColumnCount = 5;
        const brickWidth = 75;
        const brickHeight = 15;
        const brickPadding = 10;
        const brickOffsetTop = 30;
        const brickOffsetLeft = 30;
        let bricks = [];

        function initBricks() {
            bricks = [];
            for (let c = 0; c < brickColumnCount; c++) {
                bricks[c] = [];
                for (let r = 0; r < brickRowCount; r++) {
                    bricks[c][r] = { x: 0, y: 0, status: 1 };
                }
            }
        }
        initBricks();

        let score = 0;
        let lives = 3;

        function drawBall() {
            ctx.beginPath();
            ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
            ctx.fillStyle = "#0095DD";
            ctx.fill();
            ctx.closePath();
        }

        function drawPaddle() {
            ctx.beginPath();
            ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
            ctx.fillStyle = "#0095DD";
            ctx.fill();
            ctx.closePath();
        }

        function drawBricks() {
            for (let c = 0; c < brickColumnCount; c++) {
                for (let r = 0; r < brickRowCount; r++) {
                    if (bricks[c][r].status === 1) {
                        const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
                        const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
                        bricks[c][r].x = brickX;
                        bricks[c][r].y = brickY;
                        ctx.beginPath();
                        ctx.rect(brickX, brickY, brickWidth, brickHeight);
                        ctx.fillStyle = "#0095DD";
                        ctx.fill();
                        ctx.closePath();
                    }
                }
            }
        }

        function drawScore() {
            ctx.font = "16px Arial";
            ctx.fillStyle = "#0095DD";
            ctx.fillText("Score: " + score, 8, 20);
        }

        function drawLives() {
            ctx.font = "16px Arial";
            ctx.fillStyle = "#0095DD";
            ctx.fillText("Lives: " + lives, canvas.width - 65, 20);
        }

        function updateStatusMessage(msg) {
            const statusDiv = document.getElementById('gameStatus');
            if (statusDiv) statusDiv.textContent = msg;
        }

        function collisionDetection() {
            for (let c = 0; c < brickColumnCount; c++) {
                for (let r = 0; r < brickRowCount; r++) {
                    const b = bricks[c][r];
                    if (b.status === 1) {
                        if (x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
                            dy = -dy;
                            b.status = 0;
                            score++;
                            if (score === brickRowCount * brickColumnCount) {
                                gameRunning = false;
                                updateStatusMessage("🎉 ПОБЕДА! 🎉");
                            }
                        }
                    }
                }
            }
        }

        document.addEventListener('keydown', (e) => {
            if (e.key === 'ArrowRight') rightPressed = true;
            if (e.key === 'ArrowLeft') leftPressed = true;
        });
        document.addEventListener('keyup', (e) => {
            if (e.key === 'ArrowRight') rightPressed = false;
            if (e.key === 'ArrowLeft') leftPressed = false;
        });

        canvas.addEventListener('mousemove', (e) => {
            if (!gameRunning) return;
            const mouseX = e.clientX - canvas.offsetLeft;
            if (mouseX > 0 && mouseX < canvas.width) {
                paddleX = mouseX - paddleWidth / 2;
                if (paddleX < 0) paddleX = 0;
                if (paddleX > canvas.width - paddleWidth) paddleX = canvas.width - paddleWidth;
            }
        });

        function draw() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            drawBricks();
            drawBall();
            drawPaddle();
            drawScore();
            drawLives();

            if (gameRunning) {
                collisionDetection();

                if (rightPressed && paddleX < canvas.width - paddleWidth) paddleX += 7;
                if (leftPressed && paddleX > 0) paddleX -= 7;

                if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) dx = -dx;
                if (y + dy < ballRadius) dy = -dy;

                if (y + dy > canvas.height - ballRadius - paddleHeight &&
                    y + dy < canvas.height - ballRadius &&
                    x > paddleX && x < paddleX + paddleWidth) {
                    dy = -dy;
                }

                if (y + dy > canvas.height - ballRadius) {
                    lives--;
                    if (lives === 0) {
                        gameRunning = false;
                        updateStatusMessage("💀 GAME OVER 💀 Очки: " + score);
                    } else {
                        x = canvas.width / 2;
                        y = canvas.height - 30;
                        dx = 2;
                        dy = -2;
                        paddleX = (canvas.width - paddleWidth) / 2;
                        updateStatusMessage("❤️ Жизнь потеряна! Осталось: " + lives);
                    }
                }

                x += dx;
                y += dy;
            }

            requestAnimationFrame(draw);
        }

        draw();

        document.getElementById('startBtn').addEventListener('click', () => {
            gameRunning = true;
        });

        function restartGame() {
            gameRunning = true;
            lives = 3;
            score = 0;
            initBricks();
            x = canvas.width / 2;
            y = canvas.height - 30;
            dx = 2;
            dy = -2;
            paddleX = (canvas.width - paddleWidth) / 2;
            updateStatusMessage("🎮 Игра перезапущена! Удачи!");
        }

        document.getElementById('restartBtn').addEventListener('click', () => {
            restartGame();
        });
    </script>
</body>
</html>