Змейка

Напишем популярную компьютерную игру "Змейка".

Для начала нам нужно создать игровое поле при помощи Canvas. Мы зададим размеры холста, а в стиле пропишем его настройки: цвет фона, тень и т.д.

Сценарий игры разместим в отдельном файле snake.js.


<style>
canvas{
    box-shadow: black 20px 10px 50px;
}
</style>

<canvas id="board" width="400" height="400">Canvas</canvas>

<script src="snake.js"></script>

Подготовим холст к игре. Проведём его инициализацию в сценарии. И сразу создадим функцию, которая будет заниматься отрисовкой игры.


const canvas = document.getElementById('board');
const context = canvas.getContext('2d');

function drawGame() {

}

Закрасим игровое поле чёрным цветом через функцию clearScreen().


function drawGame() {
    clearScreen();
}

function clearScreen() {
    context.fillStyle = 'black';
    context.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight);
}

drawGame();

На данном этапе мы получили игровое поле чёрного цвета, на котором будет выводить игровые объекты.

Snake Board

Запустим игровой цикл.


let speed = 7; // скорость обновления экрана

function drawGame() {
    clearScreen();

    setTimeout(drawGame, 1000 / speed); // обновляем экран 7 раз в секунду
}

Разобьём игровое поле на условные квадратики, по которым будет двигаться змейка. Размеры квадратов вычисляются динамически. В каждом ряду будет 20 квадратов.


let tileCount = 20;
let tileSize = canvas.clientWidth / tileCount - 2;

Нарисуем змейку, точнее, пока только голову в центре игрового поля.


// позиция головы змейки
let headX = 10;
let headY = 10;

function drawSnake() {
    context.fillStyle = 'orange';
    context.fillRect(headX * tileCount, headY * tileCount, tileSize, tileSize);
}

function drawGame() {
    clearScreen();
    drawSnake();

    setTimeout(drawGame, 1000 / speed);
}

Snake Head

Добавим управление змейкой через клавиши-стрелки.


// слушатель нажатий клавиш
document.body.addEventListener('keydown', keyDown);

Создадим переменные для направления движения головы змейки и создадим функцию, которая будет задавать направление движения змейки после нажатий клавиш со стрелками. На основе этой информации мы можем вычислить позицию змейки


// направление движения змейки по двум осям
let xDirection = 0;
let yDirection = 0;

function keyDown(event) {
    // клавиша вверх
    if(event.keyCode == 38){
        yDirection = -1; // двигаемся на квадратик вверх
        xDirection = 0;
    }
    // клавиша вниз
    if(event.keyCode == 40){
        yDirection = 1; // на квадратик вниз
        xDirection = 0;
    }
    // клавиша влево
    if(event.keyCode == 37){
        yDirection = 0;
        xDirection = -1; // на квадратик влево
    }
    // клавиша вправо
    if(event.keyCode == 39){
        yDirection = 0;
        xDirection = 1; // на квадратик вправо
    }
}

function changeSnakePosition() {
    headX = headX + xDirection;
    headY = headY + yDirection;
}

function drawGame() {
    clearScreen();
    drawSnake();
    changeSnakePosition();

    setTimeout(drawGame, 1000 / speed); // обновляем экран 7 раз в секунду
}

На данный момент змейка может двигаться в любую сторону, не обращая внимания на границы поля и на себя. В реальной игре такое недопустимо. Змейка не может идти в обратную сторону по себе и выходить за пределы поля (тут могут быть варианты, мы берём стандартный вариант).

Давайте ограничим движение змейки в обратную сторону на себя.


function keyDown(event) {
    // клавиша вверх
    if(event.keyCode == 38){
        if(yDirection == 1)
            return; // запрещаем двигаться на себя
        yDirection = -1;
        xDirection = 0;
    }
    // клавиша вниз
    if(event.keyCode == 40){
        if(yDirection == -1)
            return; // запрещаем двигаться на себя
        yDirection = 1;
        xDirection = 0;
    }
    // клавиша влево
    if(event.keyCode == 37){
        if(xDirection == 1)
            return; // запрещаем двигаться на себя
        yDirection = 0;
        xDirection = -1;
    }
    // клавиша вправо
    if(event.keyCode == 39){
        if(xDirection == -1)
            return; // запрещаем двигаться на себя
        yDirection = 0;
        xDirection = 1;
    }
}

Яблоко

Нарисуем яблоко. Оно представляет собой квадратик поля красного цвета.


// позиция яблока
let appleX = 5;
let appleY = 5;

function drawApple() {
    context.fillStyle = 'red';
    context.fillRect(appleX * tileCount, appleY * tileCount, tileSize, tileSize);
}

function drawGame() {
    clearScreen();
    drawSnake();
    changeSnakePosition();
    drawApple();

    setTimeout(drawGame, 1000 / speed);
}

Apple

Столкновения

Теперь нужно определять моменты столкновения головы змейки с яблоком, сравнивая их позиции.


function checkCollision() {
    // если позиции яблока и головы змейки совпали
    if (appleX == headX && appleY == headY) {
        // выводим яблоко в случайном месте по горизонтали
        appleX = Math.floor(Math.random() * tileCount);
         // выводим яблоко в случайном месте по вертикали
        appleY = Math.floor(Math.random() * tileCount);
    }
}

function drawGame() {
    clearScreen();
    drawSnake();
    changeSnakePosition();
    drawApple();

    checkCollision();

    setTimeout(drawGame, 1000 / speed);
}

Тело змейки

После каждого съеденного яблока змейка увеличивается. Нам понадобится массив, который будет содержать части змейки и переменная, которая будет отвечать за размер змейки. После каждого столкновения (змейка съела яблоко) увеличиваем размер змейки.


// массив для частей змейки
const snakeParts = [];
// начальный размер змейки
let tailLength = 2;

function checkCollision() {
    if (appleX == headX && appleY == headY) {
        appleX = Math.floor(Math.random() * tileCount);
        appleY = Math.floor(Math.random() * tileCount);

        // увеличиваем размер змейки
        tailLength++;
    }
}

Нам понадобится класс, который будет содержать информацию о частях змейки.


class snakePart {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}

В функции drawSnake() добавим новый код, который будет рисовать хвост змейки к её голове.


function drawSnake() {
   context.fillStyle = 'green';
    // проходим в цикле по массиву
    for (let i = 0; i < snakeParts.length; i++) {
        // рисуем хвост змейки в виде квадратика зелёного цвета
        let part = snakeParts[i]
        context.fillRect(part.x * tileCount, part.y * tileCount, tileSize, tileSize);
    }
    // добавляем квадратики к змейке через push
    snakeParts.push(new snakePart(headX, headY));
    if (snakeParts.length > tailLength) {
        snakeParts.shift(); // убираем лишнее
    }

    context.fillStyle = 'orange';
    context.fillRect(headX * tileCount, headY * tileCount, tileSize, tileSize);
}

Змейка ожила, она есть яблоки и растёт в размерах.

Snake

Счёт

За каждое яблоко начисляется очко. Будем выводить счёт в углу игрового поля.


// счёт
let score = 0;

function drawScore() {
    context.fillStyle = 'white';
    context.font = "10px verdana";
    context.fillText("Счёт: " + score, canvas.clientWidth - 50, 10);
}

function checkCollision() {
    if (appleX == headX && appleY == headY) {
        appleX = Math.floor(Math.random() * tileCount);
        appleY = Math.floor(Math.random() * tileCount);

        tailLength++;
        // увеличиваем счёт
        score++;
    }
}

function drawGame() {
    clearScreen();
    drawSnake();
    changeSnakePosition();
    drawApple();

    checkCollision();
    drawScore();

    setTimeout(drawGame, 1000 / speed);
}

Score

Конец игры

Создадим функцию, которая будет отслеживать конец игры. Во-первых, это происходит во время столкновения со стеной, во-вторых, когда змейка кусает себя.


function isGameOver() {
    let gameOver = false;
    // проверяем начало игры
    if (yDirection === 0 && xDirection === 0) {
        return false;
    }
    if (headX < 0) {// если змейка врезалась в левую стену
        gameOver = true;
    }
    else if (headX === tileCount) {// в правую
        gameOver = true;
    }
    else if (headY < 0) {// в потолок
        gameOver = true;
    }
    else if (headY === tileCount) {// в низ
        gameOver = true;
    }

    //если змейка укусила себя
    for (let i = 0; i < snakeParts.length; i++) {
        let part = snakeParts[i];
        if (part.x === headX && part.y === headY) {
            gameOver = true;
            break;
        }
    }

    // Выводим сообщение
    if (gameOver) {
        context.fillStyle = 'white';
        context.font = "50px verdana";
        context.fillText("Конец игры! ", canvas.clientWidth / 6.5, canvas.clientHeight / 2);
    }

    return gameOver;
}

function drawGame() {
    changeSnakePosition();

    let result = isGameOver();
    if (result) {
        return;
    }

    clearScreen();
    drawSnake();

    drawApple();

    checkCollision();
    drawScore();

    setTimeout(drawGame, 1000 / speed); // обновляем экран 7 раз в секунду
}

Игра готова. Она ещё требует различных доработок, но в целом мы получили полноценную игру.

Game Over

Демо

Canvas
Реклама