JavaScript для детей — страница 43 из 48

Задаем направление движения

Свойство direction в строке  хранит текущее направление дви-

жения змейки. Также в строке  наш конструктор добавляет объекту

Next —

свойство nextDirection, где хранится направление, в котором змейка

следующий

будет двигаться на следующем шаге анимации. Это свойство будет изме-

няться в обработчике событий клавиатуры при нажатии на одну из кла-

виш-стрелок (см. «Обработчик события keydown» на с. 264). Пока что

конструктор присваивает обоим свойствам значение "right", чтобы

в начале игры змейка двигалась вправо.

Рисуем змейку

Чтобы нарисовать змейку, нам нужно перебрать в цикле все объекты-

ячейки из массива segments, вызывая методы drawSquare. Таким

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

будет нарисован квадрат.

Snake.prototype.draw = function () {

for (var i = 0; i < this.segments.length; i++) {

this.segments[i].drawSquare("Blue");

}

};

Метод draw использует цикл for для обращения к каждому из объ-

ектов-ячеек в массиве segments. При каждом повторе цикла этот код

выбирает текущий сегмент (this.segments[i]) и вызывает его метод

drawSquare("Blue"), в результате чего в соответствующей ячейке

игрового поля появится синий квадрат.

258 Часть III. Графика

Чтобы проверить этот метод, вы можете запустить следующий код,

создающий новый объект с помощью конструктора Snake и вызываю-

щий его метод draw:

var snake = new Snake();

snake.draw();

Перемещаем змейку

Для перемещения змейки на одну ячейку в текущем направ-

лении мы создадим метод move. Чтобы змейка передвинулась,

мы добавим ей новый сегмент головы (то есть добавим новый

объект-ячейку в начало массива segments), а затем удалим

из массива segments последний элемент — сегмент хвоста.

Метод move также будет вызывать метод под названием

checkCollision, проверяющий, не столкнулась ли новая голова со зме-

Collision —

иным хвостом или со стеной (рамкой), а также не съела ли новая голова

столкновение

яблоко. Если голова столкнулась со стеной или хвостом, мы завершим игру

вызовом функции gameOver, созданной в предыдущей главе. Если же змейка

съела яблоко, мы увеличим счет игры и переместим яблоко в новое место.

Добавляем метод move

Код метода move выглядит так:

Snake.prototype.move = function () {

 var head = this.segments[0];

 var newHead;

 this.direction = this.nextDirection;

 if (this.direction === "right") {

newHead = new Block(head.col + 1, head.row);

} else if (this.direction === "down") {

newHead = new Block(head.col, head.row + 1);

} else if (this.direction === "left") {

newHead = new Block(head.col - 1, head.row);

} else if (this.direction === "up") {

newHead = new Block(head.col, head.row - 1);

}

 if (this.checkCollision(newHead)) {

gameOver();

return;

}

17. Пишем игру «Змейка»: часть 2 259

 this.segments.unshift(newHead);

 if (newHead.equal(apple.position)) {

score++;

apple.move();

} else {

this.segments.pop();

}

};

Давайте разберем этот метод строчка за строчкой.

Создание новой головы

В строке  мы сохраняем первый элемент массива this.segments

(это голова змейки) в переменной head. Мы будем неодно-

кратно обращаться к первому сегменту в коде этого метода, по

-

этому использование такой переменной сделает код короче и чуть

проще для понимания. Теперь вместо того, чтобы снова и снова писать

this.segments[0], нам достаточно ввести head.

В строке  мы создали переменную newHead, чтобы использовать ее

для хранения объекта-ячейки, представляющего собой новую змеиную

голову (которую мы как раз собираемся создать).

В строке  мы присваиваем this.direction значение this.

nextDirection — после этого направление движения змейки будет

соответствовать последней нажатой клавише-стрелке (мы в подробно-

стях разберем, как это работает, когда займемся обработчиком событий

клавиатуры).

D I R E C T I O N И N E X T D I R E C T I O N

Свойство объекта-змейки direction будет

обновляться один раз за один шаг анимации,

поскольку метод move вызывается один раз

для каждого шага. С другой стороны, свойство

nextDirection будет менять значение в любой

момент, когда игрок нажмет одну из клавиш-

стрелок (и если он будет жать на клавиши

очень быстро, теоретически это свойство может

поменяться несколько раз за один шаг анимации).

Используя эти два свойства таким способом,

мы можем не бояться, что змейка мгновенно

развернется в противоположном направлении

и уткнется сама в себя, если игрок очень быстро

нажмет две кнопки в промежутке между двумя

кадрами анимации.

260 Часть III. Графика

Начиная со строки  мы используем цепочку конструкций if... else

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

змеиную голову и сохраняем ее в переменной newHead. В зависимо-

сти от направления мы добавляем или вычитаем единицу из значения

строки или столбца нынешней головы, чтобы поместить новую голову

вплотную рядом с ней (справа, слева, снизу или сверху, в зависимости

от направления движения). Например, на рис. 17.5 показано, где ока-

жется новая голова, если в this.nextDirection будет направление

"down" (вниз).

Столбцы

0

1

2

3

4

5

6

7

8

0

1

2

3

(7, 5)

4

трокиС 5

head

6

newHead

7

(7, 6)

8

newHead = new Block(head.col, head.row + 1);

Рис. 17.5. Создание новой головы,

если в this.nextDirection направление "down"

Проверка столкновения и добавление головы

В строке  мы вызываем метод checkCollision, чтобы выяснить,

не врезалась ли змейка в свой хвост или в стену. Скоро вы увидите код

этого метода, но вы, наверное, уже догадались, что он возвращает true,

если змейка на что-то наткнулась. Если это произойдет, в теле if будет

вызвана функция gameOver и игра завершится, а на экране появится

надпись «Конец игры».

Команда return в следующей за вызовом gameOver строке приве-

дет к раннему возврату из метода, так что остальной код будет пропу-

щен. Но return сработает, только если checkCollision перед этим

вернет true, поэтому, если змейка ни с чем не столкнется, будет выпол-

нен остальной код метода.

До тех пор, пока змейка не столкнется с препятствием, в строке 

мы будем добавлять ей новую голову, с помощью unshift добавляя

17. Пишем игру «Змейка»: часть 2 261

newHead в начало массива segments. Чтобы вспомнить, как работает

unshift, загляните в раздел «Добавление элементов в массив» на с. 56.

Съедение яблока

В строке  мы используем метод equal, срав-

нивая newHead и apple.position. Если пози-

ция этих объектов-ячеек одинакова, метод

equal вернет true и это будет означать, что

змейка съела яблоко.

Если яблоко съедено, мы увеличиваем счет

игры на 1 и затем вызываем для объекта apple

метод move, перемещая яблоко в новую пози-

цию. В противном случае (если змейка не съела

яблоко) мы вызываем pop для массива this.

segments. Эта команда удаляет сегмент змеиного хвоста, общая же

длина змейки остается прежней (поскольку ранее мы добавили новый

сегмент головы). Таким образом, когда змейка съедает яблоко, она ста-

новится длиннее на один сегмент, поскольку мы добавляем ей головной

сегмент, но не убираем сегмент хвоста.

Мы еще не писали код для яблока, поэтому данный метод работать

пока не будет. Однако если вам хочется его протестировать, замените

конструкцию if... else начиная со строки  на следующую строку:

this.segments.pop();

И тогда нам останется только добавить метод checkCollision, чем

мы сейчас и займемся.

Добавляем метод checkCollision

Каждый раз, когда мы задаем новую позицию змеиной головы, нужно

выполнять проверку на столкновения. Такие проверки характерны для

многих компьютерных игр, и нередко они представляют одну из наибо-

лее сложных при создании игры задач. К счастью, в нашей «Змейке» этот

вопрос решается весьма просто.

Нас волнуют только два типа столкновений: столкновения со сте-

нами и столкновения с собственным телом. Змейка может столкнуться

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

В начале игры змейка слишком коротка для этого, однако после пое-

дания нескольких яблок такое столкновение становится вполне

вероятным.

262 Часть III. Графика

Вот код метода checkCollision:

Snake.prototype.checkCollision = function (head) {

Left — слева

var leftCollision = (head.col === 0);

var topCollision = (head.row === 0);

Top — вверху

var rightCollision = (head.col === widthInBlocks - 1);

Right — справа

var bottomCollision = (head.row === heightInBlocks - 1);

Bottom — внизу

Wall — стена

 var wallCollision = leftCollision || topCollision || 

Self collision —

rightCollision || bottomCollision;

столкновение

с собой

 var selfCollision = false;


 for (var i = 0; i < this.segments.length; i++) {

if (head.equal(this.segments[i])) {

 selfCollision = true;

}

}

 return wallCollision || selfCollision;

};

Проверка столкновений со стенами