}
}
addEventListener("keydown", handler);
addEventListener("keyup", handler);
return pressed;
}
Обратите внимание, как одна функция обработчика используется для событий обоих типов. Она проверяет свойство type
объекта события, определяя, надо ли обновлять состояние кнопки на true
("keydown"
) или false
("keyup"
).
Запуск игры
Функция requestAnimationFrame
, которую мы видели в главе 13, предоставляет хороший способ анимировать игру. Но интерфейс её примитивен – его использование заставляет нас отслеживать момент времени, в который она была вызвана в прошлый раз, и вызывать requestAnimationFrame
каждый раз после каждого кадра.
Давайте определим вспомогательную функцию, оборачивающую эти скучные операции в удобный интерфейс, и позволяющую нам просто вызвать runAnimation
, задавая ей функцию, которая принимает разницу во времени и рисует один кадр. Когда функция frame
возвращает false
, анимация останавливается.
function runAnimation(frameFunc) {
var lastTime = null;
function frame(time) {
var stop = false;
if (lastTime != null) {
var timeStep = Math.min(time - lastTime, 100) / 1000;
stop = frameFunc(timeStep) === false;
}
lastTime = time;
if (!stop)
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
Я назначил максимальное время для кадра в 100 миллисекунд (1/10 секунды). Когда закладка или окно браузера спрятано, вызовы requestAnimationFrame
прекратятся, пока закладка или окно не станут снова активны. В этом случае, разница между lastTime
и текущим временем будет равна тому времени, в течение которого страница была спрятана. Продвигать игру на всё это время было бы глупо и затратно (вспомните разделение времени в методе animate
).
Эта функция также преобразовывает временные отрезки в секунды, которыми проще оперировать, чем миллисекундами.
Функция runLevel
принимает объект Level
, конструктор для display
, и, необязательным параметром – функцию. Она выводит уровень в document.body
и позволяет пользователю играть на нём. Когда уровень закончен (победа или поражение), runLevel
очищает экран, останавливает анимацию, а если задана функция andThen
, вызывает её со статусом уровня.
var arrows = trackKeys(arrowCodes);
function runLevel(level, Display, andThen) {
var display = new Display(document.body, level);
runAnimation(function(step) {
level.animate(step, arrows);
display.drawFrame(step);
if (level.isFinished()) {
display.clear();
if (andThen)
andThen(level.status);
return false;
}
});
}
Игра – это последовательность уровней. Когда игрок погибает, уровень начинается заново. Когда уровень закончен, мы переходим на следующий. Это можно выразить следующей функцией, принимающей массив планов уровней (массив строк) и конструктор display
.
function runGame(plans, Display) {
function startLevel(n) {
runLevel(new Level(plans[n]), Display, function(status) {
if (status == "lost")
startLevel(n);
else if (n < plans.length - 1)
startLevel(n + 1);
else
console.log("You win!");
});
}
startLevel(0);
}
Эти функции демонстрируют необычный стиль программирования. Обе функции runAnimation
и runLevel
– функции высшего порядка, но не в том стиле, что мы видели в главе 5. Аргумент функций используется, чтобы подготовить вещи, которые произойдут когда-либо в будущем, и функции не возвращают ничего полезного. Их задача – запланировать действия. Оборачивая эти действия в функции, мы сохраняем их как значения, чтобы их можно было вызвать в нужный момент.
Такой стиль программирования обычно называют асинхронным. Обработка событий – тоже пример такого стиля, и мы с ним встретимся ещё не раз, когда будем работать с задачами, которые могут занять произвольные промежутки времени – например, сетевые запросы в главе 17, или ввод и вывод общего назначения в главе 20.
В переменной GAME_LEVELS хранится набор планов уровней. Такая страница скармливает их в runGame
, которая запускает саму игру.
<link rel="stylesheet" href="css/game.css">
<body>
<script>
runGame(GAME_LEVELS, DOMDisplay);