Погружаясь глубже в создание пользовательских интерфейсов, вы можете столкнуться с несколькими проблемами в том, как управлять асинхронностью между функциями и событиями, которые могут происходить в определенный момент времени.

Итак, мы будем воссоздавать игру змейки, используя композицию событий с помощью Observables.

предупреждение: это не введение ни в Observables, ни в композицию событий, и в нем может быть много ошибок, на которые вы могли бы указать / могли бы указать на них, и я буду более чем счастлив принять их во внимание в качестве отзыва. .

Существует множество асинхронных примитивов, которые позволяют нам моделировать наше приложение от обратных вызовов и обещаний до Observables. Я выбрал Observables, потому что это делает поток данных естественным и плавным. По моему столь восхитительному мнению, я считаю, что они - лучший и самый забавный асинхронный примитив для создания событий на Javascript.

TLTR: останови это безумие и покажи мне код!

Для начала мы будем моделировать взаимодействие с пользователем в виде потока. Таким образом, каждый раз, когда пользователь нажимает клавишу, змейка реагирует соответствующим образом, другими словами, мы можем сопоставить клавиши с обновлениями положения змейки. Мы будем прослушивать клавиши «ArrowUp», «ArrowDown», «ArrowLeft» и «ArrowRight». но вы можете использовать другую комбинацию клавиш.

Технически мы будем прослушивать любое «нажатие клавиши», но поскольку мы явно указали, что интересуются клавишами со стрелками через функцию фильтра, это просто работает.

Как насчет того, чтобы немного разложить наш поток,

fromEvent - это наш адаптер для создания Dom API, который выполняет обратный вызов на основе потока событий. Справа ниже мы начинаем видеть первый оператор в цепочке: map, что ведет себя именно так, как мы могли ожидать, но вместо того, чтобы работать с массивами, он работает с набором значений, которые меняются с течением времени (Observables) . Но что такое оператор?

Оператор принимает Observable в качестве входных данных, а затем создает Observable в качестве выходных данных.

Учтите, что это наивная реализация на чистом Javascript, и если вы заинтересованы в более надежной реализации, перейдите к проекту rxjs.

Второй оператор - фильтр, и, как следует из названия, он фильтрует значения событий из цепочки. Но пока давайте обратим внимание на функцию isKeyAllowed.

Как вы можете заметить, он возвращает истину, когда любая клавиша со стрелкой попадает на вход. Таким образом, каждый раз, когда пользователь нажимает клавишу со стрелкой, он полностью возвращается к нам / нашей функции рендеринга (нашей функцией рендеринга может быть просто console.log).

Давайте на мгновение переосмыслим наш поток. Нет смысла называть его snakeMoves $, поскольку он уже не связан с каким-либо компонентом / концепцией, который сделал бы его семантически правильным. Тогда давай переименуем его.

Поскольку в arrowKeys $ отсутствует значение snakeMoves $, мы могли бы добавить некоторые действия / операторы, чтобы придать смысл всему конвейеру.

Это может привлечь ваше внимание к последнему действию shouldGrowBy2,, поскольку какой смысл иметь вторую функцию для выращивания змейки? но есть некоторые особые условия, которые должны быть выполнены здесь, и то, как он удлиняет и изменяет состояние змеи, просто уникально (более подходящее имя будет более подходящим, я полностью согласен).

Вы также можете задаться вопросом, зачем создавать проблемы с наличием двух потоков, если они могут быть просто одним, но пока мы продолжаем добавлять функции в нашу игру, наличие клавиш со стрелками в другом потоке просто делает ее модульной. А пока давайте просто скажем, что позже мы сделаем вторую подписку на arrowKeys $.

Позвольте мне познакомить вас с каждым действием в цепочке, чтобы мы могли согласиться, что весь этот конвейер выглядит, как движется змея:

Разрешено направление?

Простое ограничение состоит в том, что две части змеи не могут находиться в одном и том же положении одновременно, но если ваше предыдущее движение было ArrowRight, а ваше следующее движение - ArrowLeft, что доставит вам неприятности. Поэтому мы должны избегать движения в противоположных направлениях.

Переместить змею

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

Следует поменять?

Что будет, если змейка врезается в стену или границы игры? Что ж, в этом случае я предоставлю действие, чтобы изменить направление змеи, но вы, возможно, придете с совершенно другим подходом.

Должен расти?

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

Должен растиBy2?

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

Подведем итоги: snakeMoves $ - это поток, который при каждом нажатии клавиши со стрелкой вызывает изменение состояния змейки. Имея это в виду, мы могли бы определить нашу функцию рендеринга, например,

Гладко, правда? connectStore - это просто функция, которая подписывается на изменения в магазине.

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

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

Это просто оболочка для Observable. Он создает тайм-аут, очищает его, а затем создает еще один, пока не откажется от подписки.

Помните, я говорил вам, что позже мы повторно будем использовать потоки arrowKeys $? что ж, теперь самое время.

Как насчет второй подписки, чтобы обратить внимание, когда змея соприкасается с собой? на мой взгляд, довольно аккуратно, но, возможно, не с лучшей производительностью, но выглядит довольно плавно. Этот поток при запуске (или, если быть более точным, когда значение падает) отпишется от всего игрового конвейера.

Действие filter позаботится о передаче значения вниз, когда часть змеи находится в том же месте. Сброс установит для всех состояний значения по умолчанию.