пятница, 29 января 2016 г.

Reagent - вся модность ReactJS на ClojureScript

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

ClojureScript тоже решил не оставаться в стороне и какие-то ребята запили обёртку под это дело. В целом, там же Getting Started и ещё есть куча различных туториалов, где всё подробно расписано. Суть сводится к тому, что есть некоторое состояние приложения (или несколько их), которое инкапсулируется в реактивном атоме (reagent.core/atom), и есть компоненты, которые суть есть кусок DOM, изменяющийся в зависимости от состояния. Собственно, это автоматическое изменение и есть работа ReactJS. Звучит просто? Но, в целом, так оно и есть. Рассмотрим чуточку подробнее на примере игры Memory Game, исходники которого можно глянуть тут.

(def game (r/atom {}))
Который на старте игры инициализируется  следующим:
(-> game'
  (assoc-in [:level] level)
  (assoc-in [:state-changed] true)
  (assoc-in [:w] w)
  (assoc-in [:h] h)
  (assoc-in [:can-open] true)
  (assoc-in [:board] board)
  (assoc-in [:opened] [])
  (assoc-in [:opened-count] 0)
  (assoc-in [:score] 0)
  (assoc-in [:state] :playing))
Собственно, это всё, что хранится в состоянии игры. Ну, а сама инициализация рендеринга:

(r/render-component [game-component] (.getElementById js/document "app"))
Далее, сам компонент game-component является обычной функцией ClojureScript и собирает в себя остальные компоненты:

(defn game-component []
  (let [cur-game @game]
    [:div.container.container-table
     [:div.row
      [:div.col-md-3.col-md-offset-2
       [:div "Restart with level:"]
       [:button.btn.btn-xs.btn-default
        {:type "button"
         :on-click #(start :easy)}
        "Easy"]
       [:button.btn.btn-xs.btn-default
        {:type "button"
         :on-click #(start :normal)}
        "Normal"]
       [:button.btn.btn-xs.btn-default
        {:type "button"
         :on-click #(start :hard)}
        "Hard"]
       [score-component (:score cur-game)]
       [game-state-component (:state cur-game) (:score cur-game)]
       [:p "Rules: " "Open two matched cards and they will stay opened. "
        "Otherwise it will be closed, but you memorize it's locations."]
       [:p "Author: " [:a {:href "https://bitbucket.org/turtle_bazon/"} "Azamat S. Kalimoulline"]]
       [:p [:a {:href "https://bitbucket.org/turtle_bazon/cljs-games/src/"} "sources"]]]
      [board-component]]]))
При этом тут используется расширенный синтаксис квадратных скобочек, который, помимо прочего, означает, что тут необходимо вызвать функцию компонента. При этом не просто вызывать, а как-то что-то сделать и пометить там у себя, чтобы эта функция вызывалась только при изменении той ветки стейта, на основе которой он рендерится. Для этого функция должна принимать аругменты, которые как раз будут веткой (-ами) состояния.

Отдельное внимание стоит уделить компоненту board-component, который рендерит доску NxM размера. По сути, доска состоит из строк, а строки состоят из ячеек. Так вот, получается, что внутри board-component есть повторяющиеся row-component, внутри которых повторяющиеся card-component. Чтобы ReactJS смог разобраться с этим, чтобы не перерендеривать всю строку целиком, ему нужно помочь, задав некоторый идентификатор с помощью "^{:key id}":

(defn row-component [row]
  [:div.board-row
   (for [[id card] row]
     ^{:key id} [card-component id card])])

(defn board-component []
  (let [cur-game @game
        w (:w cur-game)
        h (:h cur-game)
        cards-array (partition
                     w
                     (map (fn [id card]
                            [id card])
                          (for [row (range 0 h)
                                col (range 0 w)]
                            (+ (* w row) col))
                          (:board cur-game)))]
    [:div.board.col-md-5
     (for [[row row-index] (map vector cards-array (range 0 h))]
       ^{:key (str "r" row-index)}
       [row-component row])]))
Ну а вообще, получается вполне симпатично, весь пользовательский интерфейс отрисовывается на основе состояние и изменением состояния контролируется пользовательский интерфейс. Всё чисто и понятно, если нас не подведут Reagent и ReactJS. То есть там происходит какая-то их магия, но пусть.

Итого - очень даже интересная технология, которая позволяет избавиться от жёсткого связывания модели и логики отображения. Из минусов можно отметить, что всё делается в hiccup стиле, что вынуждает верстальщика учить Clojure или, наоборот, программиста учить вёрстку. Что, по моему личному мнению, не очень хорошо. Но как раз для решения таких проблем существует проект kio, который позволяет выделить верстальщика отдельно и радоваться жизни.

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


четверг, 21 января 2016 г.

ClojureScript и Phaser.io

Недавно я писал, что так и не смог подружить ClojureScript и Phaser, и что там возникала ошибка, чего не было в Parenscript. Тогда времени было мало, сейчас же решили с virvar'ом основательно разобраться. Итого, проблему на github я закрыл. Причина же была проста. В первом примере я вообще забыл *platforms* включить физическое тело, из-за чего дальнейшие операции с ним были недоступны, а во втором варианте при использовании *platforms* не был произведён deref во время операции включения физического тела. Итого два подхода вывалились с одной ошибкой, что не позволило использовать ClojureScript на LispJam. Грустно, но се ля ви. Итого реализовал туториал на ClojureScript с использованием библиотеки phzr. Итоговые изыскания - тут.

суббота, 16 января 2016 г.

Коварные зомби съели мою корову! или история одного Lisp Game Jam

Кто-то режет салат, кто-то его ест, все поздравляют друг друга с новым годом, а мы упоролись. А упоролись такой забавной штукой как Lisp Game Jam 2016. Чтой та я решил? Да просто как-то ещё в конце декабря прошлого года в наш уютный и ламповый чатик товарищ c1tr00z запостил ссылку на предстоящий джем. Что такое джем я тогда не знал, да и сейчас не особо понимаю, но смысл описывался в том, что нужно создать игру на каком-нибудь диалекте лиспа за 7 дней. Позже дедлайн продлили ещё на три дня, но это всё вписывалось на предстоящие каникулы. В общем, подбил я c1tr00z'а, а ещё и virvar'а и понеслась.

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

Phaser

Таки сначала предстояло определиться с тем, что же использовать? Можно было использовать тру CL и cl-sdl в нём, можно было заюзать какие-нибудь биндинги к Unity3d или напилить их, но всё это было не то. Хотелось простоты дистрибутинга, а это нам давал только web. А web это яваскрипт, значит, нам нужен был яваскрипт движок. Не помню что и как искал, но нашёлся turbulenz. Первая проблема возникла после того, как я решил посмотреть что он умеет и что на нём сделано, нашёл игру Monster Force... В общем, через два дня изучения возможностей движка на примере данной игры :), скачал SDK, офигел и закрыл. Какие-то редакторы, что-то компилить и т.д. В общем, даже разбираться не стал. Начал смотреть что есть. Из всего многообразия, включая вские Panda.js и Crafty, c1tr00z таки подсказал, что есть ещё Phaser, что-то ещё и что-то ещё. Так как он был на первом месте, пошёл смотреть его. Был тутор, как создать первую игру, были другие туторы игор. Далее я делал следующее - я читал тутор phaser'а и смотрел, как там написано на яваскрипте, потом я читал доку на parenscript, чтобы написать так, чтобы он перевёл в тот яваскрипт, что на туторе, далее, собственно, это делал сам parenscipt, браузеру приходил красивый явскрипт, который, можно подумать, написал человек. В общем, квес с тутором был пройден, я его продемонстрировал и решили на phaser'е и остановиться.

Parenscript

Изначально, раз CL, то первым выходи parenscript. А это транслятор. С подмножества CL в яваскрипт. Из плюсов - неплохо так это делает, всё читается и дебажится из браузера. Минусы смутили больше, чем обрадовали плюсы - это просто транслятор, нет репла, нет среды разработки, не выработы правила создания, плагин к asdf для компилирования parenscript, который называется paren-files, имел коммит 6 (!) лет назад, с текущим ASDF3 не работает. В общем, ничего нет для parenscript. Сделали и забыли. Эта ситуация сильно удручила и я захотел реализовать это на чём-то современном, например, GWT ClojureScript. Нашёл библиотеку для clojurescript phzr и, засучив рукава, принялся делать ещё один туториал. Но почему-то body или что-то там было undefined, где оно таковым не должно было быть, и решил, что нужно использовать тру clojurescript без всяких обёрток НО ситуация повторилась один в один. Есть issue на гитхабе, где я описал свои злоключения, но ответа до сих пор нет. Что-то другое искать времени уже не было, поэтому решил, что нужно допинать parenscript, а в коде догнаться макросами. В общем, приступили на parenscript. Хорошо это или плохо пока не знаю, но конечный результат есть.

Сам джем

Как и планировалось, c1tr00z занялся исключительно графикой и основной частью гейм дизайна. Решили писать про ковбоя, который будет убивать пришельцев. Как я понял, после попытки прорисовать пришельцев, получились зомби, ну да ладно. Собственно, это была довольно нудная работа по припиливанию новых возможностей. Опять же, имя на руках паренскрипт, у нас был всего лишь яваскрипт со скобочками, что удручало. Тем не менее, использование макросов очень даже помогло. В целом, и движок phaser оказался относительно вменяемым. Дело пошло бодрее, когда подключился virvar, который думал, что 10-го джем только начинается. :) Ну а далее начал страдать дизайн кода, на который уже особо внимания не осталось в виду надвигающегося дедлайна. В общем, кое-как доделали и засабмитили под утро.

Итоги

До этого я ни в каких джемах не участвовал. Максимум, чем развлекался - это ICFPC. Это вообще разные задачи и разный экспириенс. Что же я усвоил и приобрёл?

  • главный итог в том, что игру мы эту таки сделали. Не забросили посередине, а вполне ко сроку уложились и это, считаю, огромный плюс в копилку экспы;
  • гейм индустрия очень интересна и манит кучей всего и тулов есть кучу разных. Изначально искал что-то простое, что подключил, написал, оно и готов. Вот Phaser в этом плане себя оправдал на все 100%;
  • parenscript это всё же яваскрипт со скобочками, ведь вполне присутствую проблемы с this внутри foreach и т.п. Вообще на этом джеме ояваскриптились. Но писать в скобочках всё равно приятнее. :) Но если сравнить ClojureScript и Parenscript, то, несомненно, победителем выходит ClojureScript. Ведь это действительно лисп со всеми его возможностями;
  • hunchentoot что-то не захотел работать у c1tr00z'а под виндой, что тоже насторожило;
  • близость дедлайна может превратить любой код в страшное месиво. :)
Вот. А в остальном было весело. Считаю, что каникулы прошли с пользой. Буду ли участвовать ещё в джеме? Возможно. ;)