Статьи

Создание игры Pacman с помощью Bacon.js

JavaScript охватывает асинхронное программирование. Это может быть благословением и проклятием, которое приводит к понятию «ад обратного вызова». Существуют служебные библиотеки, которые занимаются организацией асинхронного кода, такие как Async.js , но все еще сложно эффективно следовать потоку управления и рассуждать об асинхронном коде.

В этой статье я познакомлю вас с концепцией реактивного программирования, которая помогает справляться с асинхронной природой JavaScript с помощью библиотеки Bacon.js.

Давайте станем реактивными

Реактивное программирование — это асинхронные потоки данных. Он заменяет шаблон итератора на наблюдаемый шаблон. Это отличается от императивного программирования, где вы активно перебираете данные, чтобы обрабатывать вещи. В реактивном программировании вы подписываетесь на данные и реагируете на события асинхронно.

Барт Де Смет объясняет этот сдвиг в этом выступлении . В этой статье Андре Штальц подробно описывает «Реактивное программирование».

Как только вы реагируете, все становится асинхронным потоком данных: база данных на сервере, события мыши, обещания и запросы к серверу. Это позволяет вам избежать так называемого «ада обратного вызова» и обеспечивает лучшую обработку ошибок. Еще одна мощная особенность этого подхода — возможность составлять потоки вместе, что дает вам большой контроль и гибкость. Джафар Хусейн объясняет эти концепции в этом выступлении .

Bacon.js — это библиотека реактивного программирования, альтернатива RxJS . В следующих разделах мы будем использовать Bacon.js для создания версии известной игры «Pacman».

Настройка проекта

Чтобы установить Bacon.js, вы можете использовать Bower , запустив на CLI команду:

$ bower install bacon 

Как только библиотека установлена, вы готовы к работе.

API PacmanGame и UnicodeTiles.js

Для наглядности я буду использовать текстовую систему, чтобы мне не приходилось иметь дело с активами и спрайтами. Чтобы не создавать его самостоятельно, я использую потрясающую библиотеку под названием UnicodeTiles.js .

Для начала я создал класс под названием PacmanGame , который обрабатывает игровую логику. Ниже приведены методы, которые он предоставляет:

  • PacmanGame(parent) : создает игровой объект Pacman
  • start() : start() игру
  • tick() : обновляет игровую логику, отображает игру
  • spawnGhost(color) : порождает нового призрака
  • updateGhosts() : обновляет каждого призрака в игре
  • movePacman(p1V) : movePacman(p1V) Pacman в указанном направлении

Кроме того, он предоставляет следующий обратный вызов:

  • onPacmanMove(moveV) : onPacmanMove(moveV) если присутствует, когда пользователь запрашивает перемещение Pacman нажатием клавиши

Поэтому, чтобы использовать этот API, мы собираемся start игру, периодически вызывать spawnGhost чтобы вызывать призраков, прослушивать onPacmanMove вызов onPacmanMove , и всякий раз, когда это происходит, вызывать movePacman для фактического перемещения Pacman. Мы также периодически вызываем updateGhosts для обновления движений призраков. Наконец, мы периодически вызываем tick чтобы обновить изменения. И что важно, мы будем использовать Bacon.js, чтобы помочь нам в обработке событий.

Прежде чем мы начнем, давайте создадим наш игровой объект:

 var game = new PacmanGame(parentDiv); 

Мы создаем новый PacmanGame передавая родительский объект DOM parentDiv в который будет отображаться игра. Теперь мы готовы построить нашу игру.

EventStreams или Observables

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

  • observable.onValue(f) : прослушивает события-значения, это самый простой способ обработки событий.
  • observable.onError(f) : прослушивает события ошибок, полезные для обработки ошибок в потоке.
  • observable.onEnd(f) : прослушивает событие, когда поток закончился, и значение перемещения не будет доступно.

Создание потоков

Теперь, когда мы увидели основное использование потоков событий, давайте посмотрим, как их создать. Bacon.js предоставляет несколько методов, которые вы можете использовать для создания потока событий из события jQuery, обещания Ajax, DOM EventTarget, простого обратного вызова или даже массива.

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

Для большего контроля вы можете свернуть свой собственный поток событий, используя Bacon.fromBinder() . Мы покажем это в нашей игре, создав переменную moveStream , которая генерирует события для наших ходов Пакмана.

 var moveStream = Bacon.fromBinder(function(sink) { game.onPacmanMove = function(moveV) { sink(moveV); }; }); 

Мы можем вызвать sink со значением, которое отправит событие, и которое могут прослушивать наблюдатели. Вызов на sink находится в нашем onPacmanMove то есть всякий раз, когда пользователь нажимает клавишу для запроса перемещения Pacman. Таким образом, мы создали наблюдаемое, которое генерирует события о запросах на перемещение Pacman.

Обратите внимание, что мы назвали sink с простым значением moveV . Это подтолкнет события перемещения со значением moveV . Мы также можем выдвигать такие события, как Bacon.Error или Bacon.End .

Давайте создадим еще один поток событий. На этот раз мы хотим отправлять события, которые уведомляют о появлении призрака. Для этого мы создадим переменную spawnStream :

 var spawnStream = Bacon.sequentially(800, [ PacmanGame.GhostColors.ORANGE, PacmanGame.GhostColors.BLUE, PacmanGame.GhostColors.GREEN, PacmanGame.GhostColors.PURPLE, PacmanGame.GhostColors.WHITE, ]).delay(2500); 

Bacon.sequentially() создает поток, который доставляет values с заданным интервалом. В нашем случае он будет давать призрачный цвет каждые 800 миллисекунд. У нас также есть вызов метода delay() . Это задерживает поток, поэтому события начнут генерироваться после 2,5-секундной задержки.

Методы на потоках событий и мраморных диаграммах

В этом разделе я перечислю еще несколько полезных методов, которые можно использовать в потоках событий:

  • observable.map(f) : сопоставляет значения и возвращает новый поток событий.
  • observable.filter(f) : фильтрует значения с заданным предикатом.
  • observable.takeWhile(f) : принимает, пока данный предикат истинен.
  • observable.skip(n) : пропускает первые n элементов из потока.
  • observable.throttle(delay) : дросселирует поток на некоторую delay .
  • observable.debounce(delay) : Дросселирует поток на некоторую delay .
  • observable.scan(seed, f) Сканирует поток с заданным начальным значением и функцией накопителя. Это уменьшает поток до одного значения.

Для получения дополнительной информации о потоках событий см. Официальную страницу документации . Разницу между throttle и debounce можно увидеть с помощью мраморных диаграмм :

 // `source` is an event stream. // var throttled = source.throttle(2); // source: asdf----asdf---- // throttled: --s--f----s--f-- var debounced = source.debounce(2); // source: asdf----asdf---- // source.debounce(2): -----f-------f-- 

Как вы можете видеть, throttle как обычно, debounce события, тогда как debounce генерирует события только после заданного «тихого периода».

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

Наблюдение за потоком событий

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

Вспомните moveStream и spawnStream мы создали ранее. Теперь давайте подпишемся на них обоих:

 moveStream.onValue(function(moveV) { game.movePacman(moveV); }); spawnStream.onValue(function(ghost) { game.spawnGhost(ghost); }); 

Несмотря на то, что вы можете использовать stream.subscribe () , чтобы подписаться на поток, вы также можете использовать stream.onValue () . Разница в том, что subscribe будет генерировать оба типа событий, которые мы видели ранее, тогда как onValue будет onValue только события типа Bacon.Next . То есть он пропустит события Bacon.Error и Bacon.End .

Когда событие приходит в spawnStream (что происходит каждые 800 мс), его значением будет один из цветов-призраков, и мы используем этот цвет для появления призрака. Когда событие приходит в moveStream , вспомните, что это происходит, когда пользователь нажимает клавишу для перемещения Pacman. Мы называем game.movePacman с направлением moveV : оно идет вместе с событием, поэтому Pacman перемещается.

Объединение потоков событий и Bacon.Bus

Вы можете объединить потоки событий для создания других потоков. Есть много способов объединить потоки событий, вот некоторые из них:

  • Bacon.combineAsArray(streams) : объединяет потоки событий, поэтому поток результатов будет иметь массив значений в качестве значения.
  • Bacon.zipAsArray(streams) : архивирует потоки в новый поток. События из каждого потока объединяются попарно.
  • Bacon.combineTemplate(template) : объединяет потоки событий с использованием объекта шаблона.

Давайте посмотрим пример Bacon.combineTemplate :

 var password, username, firstname, lastname; // <- event streams var loginInfo = Bacon.combineTemplate({ magicNumber: 3, userid: username, passwd: password, name: { first: firstname, last: lastname } }); 

Как видите, мы объединяем потоки событий, а именно password , username , username и lastname в объединенный поток событий с именем loginInfo используя шаблон. Всякий раз, когда поток событий получает событие, поток loginInfo будет loginInfo событие, объединяя все другие шаблоны в один объект шаблона.

Существует также другой способ объединения потоков Bacon.Bus()Bacon.Bus() . Bacon.Bus() — это поток событий, который позволяет Bacon.Bus() значения в поток. Это также позволяет подключать другие потоки к шине. Мы будем использовать его для построения нашей финальной части игры:

 var ghostStream = Bacon.interval(1000, 0); ghostStream.subscribe(function() { game.updateGhosts(); }); var combinedTickStream = new Bacon.Bus(); combinedTickStream.plug(moveStream); combinedTickStream.plug(ghostStream); combinedTickStream.subscribe(function() { game.tick(); }); 

Теперь мы создаем другой поток — ghostStream , используя Bacon.interval . Этот поток будет излучать 0 каждые 1 секунду. На этот раз мы subscribe на него и вызываем game.updateGhosts для перемещения призраков. Это для перемещения призраков каждую 1 секунду. Обратите внимание на закомментированный game.tick и помните другой game.tick из нашего moveStream ? Оба потока обновляют игру и, наконец, вызывают game.tick для отображения изменений, поэтому вместо вызова game.tick в каждом потоке мы можем создать третий поток — комбинацию этих двух потоков — и вызвать game.tick в объединенном ручей.

Чтобы объединить потоки, мы можем использовать Bacon.Bus . Это последний поток событий в нашей игре, который мы называем combinedTickStream . Затем мы plug moveStream и moveStream и ghostStream , и, наконец, subscribe на него и вызываем внутри него game.tick .

И это все, мы сделали. game.start(); только запустить игру с помощью game.start(); ,

Bacon.Property и другие примеры

Бекон. Свойство , является реактивным свойством. Подумайте о реактивном свойстве, которое является суммой массива. Когда мы добавляем элемент в массив, реактивное свойство будет реагировать и обновляться само. Чтобы использовать Bacon.Property , вы можете либо подписаться на него и прослушивать изменения, либо использовать метод property.assign (obj, method) , который вызывает method данного object при каждом изменении свойства. Вот пример того, как вы могли бы использовать Bacon.Property :

 var source = Bacon.sequentially(1000, [1, 2, 3, 4]); var reactiveValue = source.scan(0, function(a, b) { return a + b; }); // 0 + 1 = 1 // 1 + 2 = 3 // 3 + 3 = 6 // 6 + 4 = 10 

Сначала мы создаем поток событий, который производит значения данного массива — 1, 2, 3 и 4 — с интервалом в 1 секунду, затем мы создаем реактивное свойство, которое является результатом scan . Это назначит значения 1, 3, 6 и 10 для значения reactiveValue .

Узнайте больше и Live Demo

В этой статье мы представили реактивное программирование с помощью Bacon.js, создав игру Pacman. Это упростило наш игровой дизайн и дало нам больше контроля и гибкости с концепцией потоков событий. Полный исходный код доступен на GitHub , а живая демонстрация доступна здесь .

Вот еще несколько полезных ссылок: