Выбрать главу

    }

  }

  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);