Статьи

Создание игры с помощью JavaScript

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

Петля

В общем, разработка игры начинается с игрового цикла. Если вы приехали из делового мира разработки программного обеспечения, это будет странно. Вы не полагаетесь на события. Фил Хаак однажды спросил меня «почему петля?», На что я ответил «э-э…». Тем не менее, гораздо лучший ответ был бы на стеке потока .

Итак, мы должны использовать мастер-цикл. Если наша среда выполнения — браузер, как настроить этот цикл? Существует относительно новый API под названием, requestAnimationFrameи это то, что большинство людей рекомендуют. Проверьте это для деталей:

(Я помню, что читал что-то негативное об API, но я не смог найти это снова.)

Я использовал requestAnimationFrameпрокладку, упомянутую в посте ирландского Пола выше. Подкладка необходима только для старых браузеров, в которых не реализован API. Кстати, мы называем каждую итерацию цикла «кадром» из-за аналогии с традиционной анимацией.

Реализация

Теперь, когда мы убедились, что requestAnimationFrameприсутствует, мы можем перейти к нашему игровому циклу. Вот загрузочный код моей игры (ну, ранняя версия):

var canvas,        // the visible canvas element    
    surface,       // the 2d context of `canvas`
    currentScreen; // the currently rendered screen for the game

function beginLoop() {
    var frameId = 0;
    var lastFrame = Date.now();

    function loop() {
        var thisFrame = Date.now();

        var elapsed = thisFrame - lastFrame;

        frameId = window.requestAnimationFrame(loop);

        currentScreen.update(elapsed);
        currentScreen.draw(surface);

        lastFrame = thisFrame;
    }

    loop();
}


canvas = document.querySelector('canvas#board');
canvas.setAttribute('width', 800);
canvas.setAttribute('height', 600);

surface = canvas.getContext('2d');

currentScreen = startScreen;
beginLoop();

Суть этой loopфункции. Имеет следующий шаг:

  • захватить текущее время
  • рассчитать время, прошедшее с последнего кадра
  • выполнить логику игры для фрейма (это вызовы update и draw)
  • запросить следующий вызов loopиспользованияrequestAnimationFrame
  • записать текущее время этого кадра для расчетов в следующем

NB Этот код еще не используется frameId. Идея в том, что этот дескриптор может быть использован для остановки цикла.

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

Большая загадка внутри loop— это currentScreenобъект. Здесь я думал о будущем (что может быть опасно). Я знаю, что в моей игре будет как минимум два «экрана», возможно, больше:

  • экран меню Пуск
  • основной экран игры (где происходит действие)

Я хотел, чтобы логика цикла работала с обоими (а также с любыми будущими экранами). Я ожидаю, что экранные объекты будут иметь два метода:

  • update занимает время, прошедшее с последнего кадра, и отвечает за обновление состояния игры.
  • draw берет контекст рисования (с холста) и отвечает за рендеринг текущего состояния игры.

Вы также увидите, что я беру элемент canvas и фиксирую его контекст рисования. Если вы не знакомы с API-интерфейсами Canvas, я рекомендую вам начать здесь .

Почему два разных метода игровой логики?

Хранение updateи drawфункции разделены важно. Когда кадры становятся дорогостоящими для вычисления, игра может реагировать с задержкой или недетерминированным поведением. Чтобы избежать этого, вы можете захотеть, чтобы ваша игра пропускала некоторую логику во время определенной итерации цикла. Однако очень вероятно, что вы не захотите отбрасывать звонки update. Если вы пропускаете рендеринг пары кадров, это не нужно , но если пропустить расчет местоположения снаряда, он может таинственным образом «пройти» через свою цель. Это станет более актуальным для нас, в частности, потому что я хотел бы, чтобы все игроки контролировали скорость игры (общая черта многих игр защиты башни).

Прямо сейчас updateи drawвсегда вызывается для каждой итерации цикла, поэтому различие является академическим в этом контексте. Мы могли бы, однако, рассчитать нашу частоту кадров loopи иногда пропускать активацию, drawесли скорость замедлилась.

Теперь у нас достаточно места, чтобы начать работу над экраном меню «Пуск».