Напишем популярную компьютерную игру "Змейка".
Для начала нам нужно создать игровое поле при помощи 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();
На данном этапе мы получили игровое поле чёрного цвета, на котором будет выводить игровые объекты.
Запустим игровой цикл.
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);
}
Добавим управление змейкой через клавиши-стрелки.
// слушатель нажатий клавиш
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);
}
Теперь нужно определять моменты столкновения головы змейки с яблоком, сравнивая их позиции.
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);
}
Змейка ожила, она есть яблоки и растёт в размерах.
За каждое яблоко начисляется очко. Будем выводить счёт в углу игрового поля.
// счёт
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);
}
Создадим функцию, которая будет отслеживать конец игры. Во-первых, это происходит во время столкновения со стеной, во-вторых, когда змейка кусает себя.
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 раз в секунду
}
Игра готова. Она ещё требует различных доработок, но в целом мы получили полноценную игру.