Пишем игру змейка с помощью JavaScript + Canvas

в 12:30, , рубрики: canvas, Gamedev, html, html5, javascript, snake

Доброго времени суток, друзья. Сейчас я постараюсь вам показать как можно написать игру Змейка. Конечно, не самым быстрым способом и не самым маленьким в плане количества строк кода, но по-моему самым понятным для начинающих разработчиков, как я. Статья написана для людей, желающих чуть-чуть познакомиться с элементом canvas и его простыми методами для работы с 2D графикой.
image
Напишем змейку в «старом» виде, без особо красивой графики — в виде кубиков. Но это только упростит понимание разработки. Ну что же, поехали!

Подготовка

Пожалуй, стоит вообще начать с подготовки к созданию игры и написанию кода. Мы будем использовать простой редактор Sublime Text. Впрочем, это не важно. Все делаем в одном документе, чтобы было быстрее.

Первым делом, напишем сам код для встраивания canvas в документ. Напомню, что canvas поддерживается только в HTML5.

<!-- Надпись по середине появиться в случае, если у вас старый браузер. -->
<canvas id="gP">HTML5 не поддерживается</canvas>

Подготовка завершена, теперь мы можем приступать к созданию самой игры.

Начинаем

Для начала, я хотел бы вам вообще объяснить как будет работать змейка, так будет гораздо понятнее. Наша змейка — это массив. Массив элементов, элементы — это ее части, на которые она делиться. Это всего лишь квадратики, которые имеют координаты X и Y. Как вы знаете, X — горизонталь, Y — вертикаль. В обычном виде мы представляем себе координатную плоскость вот так:

image

Она абсолютно правильная, в этом нет сомнения, но на мониторе компьютера (в частности, canvas) она выглядит по-другому, вот так:

image

Это нужно знать, если вы вдруг в первый раз столкнулись с canvas. Я, когда столкнулся с этим, сначала вообще не понял где точка (0,0), благо я быстро разобрался. Надеюсь и у вас проблем не возникло.

Вернемся к элементам змейки, ее частям. Представим, что каждый элемент имеет свои координаты, но одинаковую высоту и одинаковую ширину. Это квадратики, не более. А теперь представим, что мы вот нарисовали змейку, все квадратики, они идут друг за другом, одинаковые такие. И тут мы решили, что нам нужно подвинуть змейку вправо. Как бы вы поступили?

Некоторые люди ответили бы, что нам нужно первый элемент подвинуть вправо, затем второй, затем третий и так далее. Но для меня этот вариант не является правильным, так как в случае, если вдруг змейка огромная, а компьютер слабый, мы можем заметить, что змейка иногда разрывается, что вообще не должно быть. Да и вообще, данный способ требует слишком много команд, когда можно обойтись гораздо меньшим количеством, не потеряв качество. А теперь мой способ: Мы берем последний элемент змейки, и ставим его в начало, изменяя его координаты так, чтобы он был после головы. Теперь этот элемент — голова. Всего-то! И да, эффект движения будет присутствовать, а на компьютере вообще не будет заметно, как мы спрятали хвост, а потом его поставили в начало. именно так мы и будем поступать во время создания движения змейки.

Вот тут точно начинаем

Сначала нам нужно объявить некоторые переменные, которые мы в дальнейшем будем использовать.

//Возвращает случайное число.
function rand (min, max) {k = Math.floor(Math.random() * (max - min) + min); return (Math.round( k / s) * s);}
//Функция для создания нового яблока.
function newA () {a = [rand(0, innerWidth),rand(0, innerHeight)];}
//Функция для создания тела змейки из одного элемента.
function newB () {sBody = [{x: 0,y: 0}];}

var gP = document.getElementById('gP'), //Достаем canvas.
	//Получаем "контекст" (методы для рисования в canvas).
        g = gP.getContext('2d'), 
	sBody = null, //Тело змейки, мы потом его создадим.
	d = 1, //Направление змейки 1 - dправо, 2 - вниз 3 - влево, 4 - вверх.
	a = null, //Яблоко, массив, 0 элемент - x, 1 элемент - y.
	s = 30; newB(); newA(); //Создаем змейку.

И так, вообще, нужно чуть-чуть пояснить зачем нам нужно в функции возвращения случайного числа, умножать и делить на переменную s, которая хранит в себе ни что иное, как ширину, по совместительству и высоту элементов змейки. На самом деле, это нужно, чтобы не было смещений во время движения, так как у нас ширина элемента — 30, то если мы хотим двигать ее без разрывов, то все координаты должны делиться на 30 без остатка. Именно поэтому я делю на число, округляю, а потом умножаю. Таким образом, число возвращается таким, что его можно разделить без остатка на 30.

Вы могли бы возразить, сказав, что ты мог бы просто холсту сделать ширину и высоту, кратную 30. Но на самом деле, это не лучший вариант. Так как я лично привык использовать всю ширину экрана. И в случае, если ширина = 320, то мне пришлось бы аж целых 20 пикселей забирать у пользователя, что могло бы доставить дискомфорт. Именно поэтому в нашей змейки все координаты объектов делятся на 30, чтобы не было никаких неожиданных моментов. Было бы даже правильнее вынести это как отдельную функцию, так как она достаточно часто используется в коде. Но к этому выводу я пришел поздно. (Но возможно это даже не нужно).

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

gP.width = innerWidth; //Сохраняем четкость изображения, выставив полную ширину экрана.
gP.height = innerHeight; //То же самое, но только с высотой.

Если что, то переменные innerWidth и innerHeight хранятся в глобальном пространстве имен.
Поэтому к ним можно обратиться именно так. Правда, я не знаю правильно ли так делать.

Ну что же, теперь начинаем писать код змейки.

Чтобы было движение, нам нужна анимация, мы будем использовать функцию setInterval, вторым параметром которой будет число 60. Можно чуть больше, 75 на пример, но мне нравится 60. Функция всего на всего каждые 60 мс. рисует змейку «заново». Дальнейшее написание кода — это только этот интервал.

Покажу вообще простую отрисовку нашей змейки, пока что без движения.

setInterval(function(){
	g.clearRect(0,0,gP.width,gP.height); //Очищаем старое.
	g.fillStyle = "red"; //Даем красный цвет для рисования яблока.
	g.fillRect(...a, s, s); //Рисуем яблоко на холсте 30x30 с координатами a[0] и a[1].
	g.fillStyle = "#000"; //А теперь черный цвет для змейки.
}, 60);

Чтобы проверить, что наша змейка не сталкивается сама с собой, нам нужно сделать некоторую проверку для каждого элемента, кроме последнего. Мы будем проверять, не равны ли координаты последнего элемента (головы) змейки любым из… То есть проще говоря: не произошло ли столкновение. Эта строчка кода была единой строкой, но вам сделал ее понятной. Напоминаю, что все это добавляется в функцию интервала.


sBody.forEach(function(el, i){
   
    //Проверка на то, что яблоко ушло за границы окна, мы его не можем увидеть.
    if (a[0] + s >= gP.width || a[1] + s >= gP.height) newA();

    //Проверка на столкновение.
    var last = sBody.length - 1;
    if ( el.x == sBody[last].x && el.y == sBody[last].y && i < last) { 
        sBody.splice(0,last); //Стираем тело змейки.
        sBody = [{x:0,y:0}]; //Создаем его заново.
        d = 1;  //Меняем направление на правую сторону.
    }

});

//+
// Сохраняем хвост и голову змейки.
var m = sBody[0], f = {x: m.x,y: m.y}, l = sBody[sBody.length - 1];

/*

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

    Делается это путем проверки направления змейки (изначально -  это 1, - право), 
    а затем уже изменяем координаты. Соответственно, комментарии все описывают.

*/


//Если направление вправо, то тогда сохраняем Y, но меняем X на + s.
if (d == 1)  f.x = l.x + s, f.y = Math.round(l.y / s) * s;
// Если направление вниз, то сохраняем X, но меняем Y на + s.
if (d == 2) f.y = l.y + s, f.x = Math.round(l.x / s) * s;
//Если направление влево, то сохраняем Y, но меняем X на -s.
if (d == 3) f.x = l.x - s, f.y = Math.round(l.y / s) * s;
//Если направление вверх, то сохраняем X, Но меняем Y на -s.
if (d == 4) f.y = l.y - s, f.x = Math.round(l.x / s) * s;
 

А теперь, вы наверное заметили, что во время того, как мы изменяем координаты, мы вечно что-то «сохраняем», сначала поделив, а потом округлив и умножив на число s. Это все тот же самый способ выравнивания змейки относительно яблока. Движение в данном случае строгое, простое, поэтому и есть змейка яблоко может строго по определенным правилам, которые задан в самом начале интервала. И если бы координаты головы змейки хоть на 1px сместились бы, то яблоко нельзя было бы съесть. И да, это простой вариант, поэтому все так сильно ограничено.

Ну а нам же осталось что сделать? Правильно, удалить из массива хвост (первый элемент), добавить новый элемент в самый конец и отрисовать всю змейку. Сделаем это, добавив в конец интервала вот такие строчки кода.


sBody.push(f); //Добавляем хвост после головы с новыми координатами.
sBody.splice(0,1); //Удаляем хвост.

//Отрисовываем каждый элемент змейки.
sBody.forEach(function(pob, i){
    //Если мы двигаемся вправо, то если позиция элемента по X больше, чем ширина экрана, то ее надо обнулить
    if (d == 1) if (pob.x > Math.round(gP.width / s) * s) pob.x = 0;
    //Если мы двигаемся вниз, то если позиция элемента по X больше, чем высота экрана, то ее надо обнулить.
    if (d == 2) if (pob.y > Math.round(gP.height / s) * s) pob.y = 0;
   //Если мы двигаемся влево, и позиция по X меньше нуля, то мы ставим элемент в самый конец экрана (его ширина).
    if (d == 3) if (pob.x < 0) pob.x = Math.round(gP.width / s) * s;
    //Если мы двигаемся вверх, и позиция по Y меньше нуля, то мы ставим элемент в самый низ экрана (его высоту).
    if (d == 4) if (pob.y < 0) pob.y = Math.round(gP.height / s) * s;
   
    //И тут же проверка на то, что змейка съела яблоко.
    if (pob.x == a[0] && pob.y == a[1]) newA(), sBody.unshift({x: f.x - s, y:l.y})
    
    //А теперь рисуем элемент змейки.
    g.fillRect(pob.x, pob.y, s, s);		
});

В добавок к отрисовке змейки, я добавил код, который делает ощущение, что конец экрана — это его начало. И если змейка выходит за границы, то она потом выходит из начала, на погибая.
Вы можете заменить обнуление координат, на пример, на сбрасывание игры, если у вас все очень жестко. Но мне нравится больше так. Ну а теперь, осталось только по нажатию кнопок изменять направление змейки. Делает за считанные секунды. Нужно лишь написать этот код сразу после setInterval. Примерно так:

//setInerval(...);

onkeydown = function (e) {
	var k = e.keyCode;
	if ([38,39,40,37].indexOf(k) >= 0) e.preventDefault();
	if (k == 39 && d != 3) d = 1; //Вправо
	if (k == 40 && d != 4) d = 2; //Вниз
	if (k == 37 && d != 1) d = 3; //Влево
	if (k == 38 && d != 2) d = 4; //Вверх
};

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

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

Автор: kinojs

Источник


  1. Ыусещк:

    Попытался реализовать, но что-то не вышло…
    https://www.youtube.com/watch?v=GA9SftvhiuE

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js